*CTF 2021 lottery again

编程入门 行业动态 更新时间:2024-10-11 01:20:44

*<a href=https://www.elefans.com/category/jswz/34/1760780.html style=CTF 2021 lottery again"/>

*CTF 2021 lottery again

Start

给了部分源码,先理一遍这个站的逻辑,
首先index.html中是一个登录框,该登录框可以任意注册:

每个新用户有coin:300,可以通过买彩票进行赚钱,赚够9999,即可买到flag:

然后查看源码,找到买彩票的关键代码:

app.Http.Controllers.LotteryController.php 28 line:
$lottery = Lottery::create(['coin' => 100 - floor(sqrt(random_int(1, 10000)))]);

很明显,通过买彩票赚coins只会越来越少,只能另谋出路

bp,抓包,尝试购买彩票:
第一个包:
这里api_token就是每个账户的身份验证,

买完之后返回enc:

app.Http.Controller.LotteryController 34 line:
$enc = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, env('LOTTERY_KEY'), $serilized, MCRYPT_MODE_ECB));

这里enc的加密是通过mcrypt进行RIJNDAEL_256的ECB模式加密,加密的key为服务器上的一个LOTTERY_KEY的环境变量的值,所以这个key看样子是无法获取到的,那么把目标转移到ECB模式的缺陷下,
首先对于ECB的加密是分组进行加密的,然后是rijndael_256的加密是以32字节为一组的
对于$serialized:

$serilized = json_encode(['lottery' => $lottery->uuid,'user' => $user->uuid,'coin' => $lottery->coin,
]);

先往下看到第二个包:

这个包只是解析了enc,将enc的值解析为info对象

app.Http.Controller.LotteryController 41 line:public function info(Request $request){return ['info' => $this->decrypt($request->input('enc')),];}

可以看到在这个位置调用了私有方法decrypt:

app.Http.Controller.LotteryController 78 line:
private function decrypt($enc){$serilized = trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, env('LOTTERY_KEY'), base64_decode($enc), MCRYPT_MODE_ECB));$info = json_decode($serilized);if (empty($info)) {throw new Exception('invalid lottery');}return $info;}

利用泄露的info接口生成的info对象构造json_encode:

$serilized = json_encode(["lottery" => "28ee49a1-e423-4d0e-a372-82779cb4d3b6","user" => "55ade85f-a142-4268-9c6c-7c890b604628","coin" => 24,
]);
echo $serialized;

可以得到:

{"lottery":"28ee49a1-e423-4d0e-a372-82779cb4d3b6","user":"55ade85f-a142-4268-9c6c-7c890b604628","coin":24}

32个字节为一组分组得到(不足的补0):

{"lottery":"28ee49a1-e423-4d0e-a372-82779cb4d3b6","user":"55ade85f-a142-4268-9c6c-7c890b604628","coin":24}0000000000000000000000

总共可以分为4组,得到的结果就是128个字节的数据,将生成的enc通过base64解码,刚好是128个字节:

观察分组后的每一组数据,可以发现除了最后一组,其他组都是无法完全控制的,也就是说,lottery和user都不可控,注意到加密的时候用到了json_encode

$serilized = json_encode(["lottery" => "28ee49a1-e423-4d0e-a372-82779cb4d3b6","user" => "55ade85f-a142-4268-9c6c-7c890b604628","user" => "test","coin" => 24,
]);
echo $serilized;
echo "<br>";
var_dump(json_decode($serilized));

说明,在json_encode()的时候,如果键同名,位置靠后的键值会覆盖位置靠前的键值,那么就可以通过覆盖前面的user值,进行重放攻击,测试:
enc1:

{"lottery":"28ee49a1-e423-4d0e-a  372-82779cb4d3b6","user":"55ade8  5f-a142-4268-9c6c-7c890b604628",  "coin":24}

enc2:

{"lottery":"3457f8f7-63c4-47b4-a  514-5bbfff6bd205","user":"a55b9f  5d-5c37-46ab-a0f2-e2a73e62498b",  "coin":9999}

将enc1覆盖为enc2的userid:

{"lottery":"28ee49a1-e423-4d0e-a 372-82779cb4d3b6","user":"55ade8372-82779cb4d3b6","user":"55ade8514-5bbfff6bd205","user":"a55b9f5d-5c37-46ab-a0f2-e2a73e62498b",  "coin":9999}

前面的user为:"user":"55ade8372-82779cb4d3b6",不重要,json_encode调用的时候就已经将该值覆盖了。
继续往下看第三个包:
这里我们发现,如果不点击charge的时候,你的钱是不会加到现在的账户上的,而是需要通过点击charge之后,才会将coin值加到对应的userid


那么该位置应该就是漏洞的利用点,通过这个charge函数以及前面说到的覆盖userid的方法将别人的coin不断的输送到我的userid上,
接下来就是构造payload进行循环了,需要确定好一点,对于每个用户,其token以及uuid在创建账户的时候就已经确定了,也就是说每个tokenuuid都对应唯一的用户,这样就可以通过token或者uuid确定需要charge的对象了。

exp.py(该exp改自星盟CTF战队的exp)

from base64 import b64encode as be
from base64 import b64decode as bd
import requests as req
import random
import string
import json
from urllib.parse import quoteurl = 'http://52.149.144.45:8080'my_userid = 'eccf5644-b530-49c5-8203-051b4513d96f'
my_enc = b'6eCIha3RKgcFpe2eFd6HJUK9Lob9PkInGgz+mB3jiH41e1fbL368t8s9svcCP1zjan8G8X\/HkAdCk18fZjYTWVlweXeUei4\/OZimnuVYcuLjsnlDHHQPYdhoweu7dEdgxhM49mxRLoJBoYZf\/RWYUQGtDLpwBs66+iCUvumfyqE='
cookie = {'api_token':'GrJQBeW0x59Ss1XLu5CArKutkJYUVmfJ'
}def get_random_username():return ''.join(random.sample(string.ascii_letters + string.digits, 12))def register():username = get_random_username()data = {'username': username,'password': '123456'}res = req.post(url=url+'/user/register', data=data)if 'duplicate' in res.text:  return 1return usernamedef login(username):data = {'username': username,'password': '123456'}res = req.post(url=url+'/user/login', data=data)d = json.loads(res.text)return d['user']['api_token']def info(api_token):res = req.get(url + "/user/info?api_token=" + api_token)d = json.loads(res.text)print('uuid: '+d['user']['uuid'])def buy(api_token):data = {'api_token': api_token}res = req.post(url=url+'/lottery/buy', data=data)return json.loads(res.text)['enc']def get_enc(enc):i = bd(enc)ii = bd(my_enc)enc3 = be(i[:64] + ii[32:])# print('enc: ', end='')# print(quote(enc3))return enc3def charge(enc):data = {'user': my_userid,'coin': 51,'enc': enc}res = req.post(url=url+'/lottery/charge', data=data)if 'invalid' in res.text:return Falsereturn Trueif __name__ == '__main__':while True:username = register()if username == 1:continueapi_token = login(username)info(api_token)enc = get_enc(buy(api_token))if charge(enc):print('True')else:print('False')

经过n秒之后:

最终:

参考文章:*CTF 2021 writeup by 星盟CTF战队
这道题当时写的时候并没有想到可以这样覆盖,json_decode的位置也是一直绕不过去,一直在蹲writeup,如今writeup终于出来了,感谢星盟的师傅醍醐灌顶。

End

更多推荐

*CTF 2021 lottery again

本文发布于:2024-02-07 10:07:09,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1756530.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:CTF   lottery

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!