2023年3月

今天有时间自己尝试了一下os.walk的小实验,结果出现了一个小问题:在交互模式下,运行我的python脚本,没有打印任何内容

返回去看一下test.py内容

返回去看一下文件路径是否正确:

看着好像没有任何问题。。。

然后仔细想了想发现,可能是运行的脚本test.py与想要访问的目录中的test2存在相同字段,导致python不能正确区分,然后就是验证过程:

首先将文件名称test2改为其他(ASAD)

然后修改python文件内的路径:

再次在交互模式下运行:

哇,可以看到运行成功了,目录及子目录下的文件夹、文件都打印出来了

总结:打印失败就是因为py脚本名称与路径名称存在重复导致的。虽然是个小问题,但是很容易被忽略。尤其是类似test*这种常用名称

上篇文章我们介绍了

VUE+.NET应用系统的国际化-整体设计思路

系统国际化改造整体设计思路如下:

  1. 提供一个工具,识别前后端代码中的中文,形成多语言词条,按语言、界面、模块统一管理多有的多语言词条
  2. 提供一个翻译服务,批量翻译多语言词条
  3. 提供一个词条服务,支持后端代码在运行时根据用户登录的语言,动态获取对应的多语言文本
  4. 提供前端多语言JS生成服务,按界面动态生成对应的多语言JS文件,方便前端VUE文件使用。
  5. 提供代码替换工具,将VUE前端代码中的中文替换为$t("词条ID"),后端代码中的中文替换为TermService.Current.GetText("词条ID")

今天,我们继续介绍多语言词条服务的设计和实现。

一、多语言词条设计

什么是多语言词条,即代码中需要支持多语言的文本。例如后台提示、前端界面的各类显示元素(Label、Button文字、Tooltips、标题、列表标题等等)。这些内容统一抽象为多语言词条。

多语言词条是产品多语言包的组成部分。支持在不同的语言下,显示对应的文本。

上图中:

I18NTerm代表多语言词条对象,主要描述了多语言词条的各个属性,主要的几个属性有:

/// <summary>
        ///词条的key/// </summary>
        public string Code { get; set; }/// <summary>
        ///词条的名称/// </summary>
        public string Name { get; set; }/// <summary>
        ///原始文本/// </summary>
        public string OriginalText { get; set; }/// <summary>
        ///多语言词条子项/// </summary>
        public List<I18NTermItem> TranslateItems { get; set; } = new List<I18NTermItem>();/// <summary>
        ///隶属的产品/// </summary>
        public string Product { get; set; }/// <summary>
        ///隶属的关键应用/系统/// </summary>
        /// <remarks>
        ///用于批量打包国际化JS文件/// </remarks>
        public string SubSystem { get; set; }/// <summary>
        ///隶属的关键应用/系统编号/// </summary>
        /// <remarks>
        ///用于批量打包国际化JS文件/// </remarks>
        public string SubSystemCode { get; set; }

一条词条,包含多个词条子项I18NTermItem,每一个词条子项,都代表了一种语言的翻译结果

 public classI18NTermItem : CacheElement
{
/// <summary> ///词条ID/// </summary> public string TermID { get; set; }/// <summary> ///语言/// </summary> public string Language { get; set; }/// <summary> ///翻译的文本/// </summary> public string TranslateText { get; set; }/// <summary> ///用户自定义文本/// </summary> public string CustomText { get; set; }public stringGetText()
{
if (string.IsNullOrEmpty(CustomText))
{
returnTranslateText;
}
returnCustomText;
}
}

二、多语言词条管理服务

有了多语言词条对象后,需要增加其对应的多语言词条管理服务,用于对词条的增删查改

先定义一个多语言词条管理的接口II18NTermManageService

public interfaceII18NTermManageService
{
voidAdd(I18NTerm term);void Remove(stringtermId);void AddTerms(List<I18NTerm>terms);void RemoveTerms(List<string>terms);voidUpdate(I18NTerm term);

I18NTerm GetTerm(
stringtermId);

List
<I18NTerm>GetTerms();

List
<I18NTerm> GetTerms(stringsourceId);

List
<I18NTerm> GetTermsByApplication(stringapplicationId);

List
<I18NTerm> GetTermByConditions(string applicationId, string sourceId = null, string sourceLocation = null, string Dimension1 = null, string Dimension2 = null, string Dimension3 = null);

}

这个接口对应的实现中,可以采用EF完成词条数据的持久化操作,在这里不再详细展示了,大家根据需求自行实现即可。

三、多语言词条查询服务

系统在运行时,需要调用词条服务查询各类词条的翻译文本。因此,抽象一个多语言词条查询服务接口II18NTermService

    /// <summary>
    ///词条查询服务接口/// </summary>
    public interfaceII18NTermService
{
/// <summary> ///根据词条编号获取对应的词条翻译/// </summary> /// <param name="termCode">词条编号</param> /// <param name="defaultText">默认值,如果根据编号找不到词条或者词条对应的翻译将返回默认值</param> /// <returns></returns> string GetText(string termCode, stringdefaultText);/// <summary> ///根据词条编号获取对应的词条翻译并格式化输出/// </summary> /// <param name="termCode">词条编号</param> /// <param name="defaultText">默认值,如果根据编号找不到词条或者词条对应的翻译将返回默认值</param> /// <param name="args">包含零个或多个要格式化的对象的对象数组</param> /// <returns></returns> string GetTextFormatted(string termCode, string defaultText, params object[] args);/// <summary> ///根据词条编号获取对应的词条翻译/// </summary> /// <param name="termCode">词条编号</param> /// <param name="language">语言标识</param> /// <param name="defaultText">默认值,如果根据编号找不到词条或者词条对应的翻译将返回默认值</param> /// <returns></returns> string GetTextWithlanguage(string termCode,string language, stringdefaultText);/// <summary> ///根据词条编号获取对应的词条翻译并格式化输出/// </summary> /// <param name="termCode">词条编号</param> /// <param name="language">语言标识</param> /// <param name="defaultText">默认值,如果根据编号找不到词条或者词条对应的翻译将返回默认值</param> /// <param name="args">包含零个或多个要格式化的对象的对象数组</param> /// <returns></returns> string GetTextFormattedWithlanguage(string termCode, string language, string defaultText, params object[] args);/// <summary> ///批量获取词条,注意:此接口不能在特来电生产环境使用。/// </summary> /// <param name="termCodes"></param> /// <returns></returns> Dictionary<string,string> BatchGetText(List<string>termCodes);
}

这个接口的具体实现中,可以增加词条的Redis缓存和内存缓存,调用II18NTermManageService的实现逻辑,从数据库中查询持久化的词条数据。缓存到内存和Redis中,  以提升查询性能。

例如:

 /// <summary>
        ///获取词条翻译/// </summary>
        /// <param name="termCode">词条编号</param>
        /// <param name="defaultText">默认值,当找不到对应的词条时将返回默认值</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException"></exception>
        public string GetText(string termCode, stringdefaultText)
{
if (string.IsNullOrWhiteSpace(termCode))throw new ArgumentNullException($"Term Code is null, {termCode}");if (Teld.Core.Session.Service.AppContext.Current.Language == null)
{
returndefaultText;
}
string language =T.Core.Session.Service.AppContext.Current.Language.DisplayCode;string key = termCode + "&" +language;if (cache.TryGetValue(key, out varval))
{
returnval;
}
var termItem =termManageService.GetTermItem(termCode, language);if (termItem == null)
{
TermMonitor.NotFound(termCode, language);
returndefaultText;
}
else{string text =termItem.GetText();
cache[key]
=text;returntext;
}
}

以上是多语言词条服务的设计和实现。

分享给大家

周国庆

2023/3/11

1. Gin简介

前面通过两篇文章分享了Golang HTTP编程的路由分发、请求/响应处理。

可以看出来Golang原生HTTP编程在路由分组、动态路由及参数读取/验证、构造String/Data/JSON/HTML响应的方法等存在优化的空间。

Gin是一个用Golang编写的高性能Web框架。

  • 基于前缀树的路由,快速且支持动态路由
  • 支持中间件及路由分组,将具有同一特性的路由划入统一组别、设置相同的中间件。
    • 比如需要登录的一批接口接入登录权限认证中间件、而不需要登录一批接口则不需要接入
  • ...

2. 快速使用

基于
gin@v1.8.1
,基本使用如下

func main() {
    // Creates a new blank Engine instance without any middleware attached
    engine := gin.New()
    // Global middleware
    // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
    // By default gin.DefaultWriter = os.Stdout
    engine.Use(gin.Logger())
    // Recovery middleware recovers from any panics and writes a 500 if there was one.
    engine.Use(gin.Recovery())
    v1Group := engine.Group("app/v1", accessHandler())
    v1Group.GET("user/info", userInfoLogic())
    engine.Run(":8019")
}

终端运行
go run main.go
,输出如下

$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /app/v1/user/info         --> main.userInfoLogic.func1 (4 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8019

通过打印可以看出注册了GET方法的路由
/app/v1/user/info
,对应处理函数为
main.userInfoLogic

总共包括四个处理函数,按顺序为
gin.Logger()

gin.Recovery()

accessHandler()
以及
userInfoLogic

最终在端口8019启动了HTTP监听服务。

2.1 创建
Engine
并使用
gin.Logger()

gin.Recovery()
两个全局中间件,对
engine
下的所有路由都生效

通过代码及注释,
gin.Logger()

gin.Recovery()
放到了
Engine.RouterGroup.Handlers
切片中。

// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

2.2 创建路由分组
v1Group
,且该分组使用了
accessHandler()

accessHandler()

v1Group
分组路由均生效

// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
    return &RouterGroup{
        Handlers: group.combineHandlers(handlers),
        basePath: group.calculateAbsolutePath(relativePath),
        engine:   group.engine,
    }
}

从代码可以看出,返回了新的
gin.RouterGroup
,并且

v1Group.Handlers = append(group.Handlers, handlers)
,此时
gin.RouterGroup.Handlers

[gin.Logger(),gin.Recovery(),accessHandler()]

同时
v1Group.basePath = "app/v1"

从代码同时可以得出,支持分组嵌套分组。即在
v1Group
都基础上在创建分组,比如
v1Group.Group("north")

2.3 在
v1Group
下注册路由
user/info
,该路由的处理函数是
userInfoLogic
,方法为
GET

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath) // 计算出完整路由
    handlers = group.combineHandlers(handlers) // 将新处理函数拼接到原来的末尾
    group.engine.addRoute(httpMethod, absolutePath, handlers) // 路由加入到前缀树
    return group.returnObj()
}
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    ...
    root := engine.trees.get(method)
    if root == nil {
        root = new(node)
        root.fullPath = "/"
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
	root.addRoute(path, handlers)
    ...
}

将分组
v1Group
的路由前缀和当前
user/info
计算得到完整路由,即
app/v1/user/info

合并处理函数,此时
handlers = [gin.Logger(),gin.Recovery(),accessHandler(),userInfoLogic()]

最后将路由及处理函数按http method分组,加入到不同路由树中。

2.4 通过
engine.Run(":8019")
在启动HTTP服务

// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
    ...
    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine.Handler())
    return
}

这里调用
http.ListenAndServe
启动
HTTP
监听服务,
Engine
实现了
http.Handler
接口,如果有客户端请求,会调用到
Engine.ServeHTTP
函数。

3. 路由过程

// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    ...
    // Find root of the tree for the given HTTP method
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method != httpMethod {
            continue
        }
        root := t[i].root
        // Find route in tree
        value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
        if value.params != nil {
            c.Params = *value.params
        }
        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }
        ...
        break
    }
}
// context.go
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

从上面代码可以看出,通过http method找到对应的路由树,再根据URL从路由树中查找对应的节点,

获取到处理函数切片,通过
c.Next
按通过顺序执行处理函数。

对于请求GET /app/v1/user/info,将依次执行
[gin.Logger(),gin.Recovery(),accessHandler(),userInfoLogic()]

4. 请求/响应参数处理

func accessHandler() func(*gin.Context) {
    return func(c *gin.Context) {
        // 不允许crul访问
        if strings.Contains(c.GetHeader("user-agent"), "curl") {
            c.JSON(http.StatusBadRequest, "cant't not visited by curl")
            c.Abort() // 直接退出,避免执行后续处理函数
        }
    }
}
func userInfoLogic() func(*gin.Context) {
    return func(c *gin.Context) {
        id := c.Query("id")
        c.JSON(http.StatusOK, map[string]interface{}{"id": id, "name": "bob", "age": 18})
	}
}

v1Group
的通用处理函数
accessHandler
,达到
v1Group
下注册的路由无法用curl访问的效果。

通过
c.Query("id")
获取URL查询参数,

通过以下代码可以看出,第一次获取URL查询时会缓存所有URL查询参数,这减少了内存的分配,节省了计算资源。

因为每次调用
url.ParseQuery
都会重新申请缓存,重复解析URL。

func (c *Context) Query(key string) (value string) {
    value, _ = c.GetQuery(key)
    return
}
func (c *Context) initQueryCache() {
    if c.queryCache == nil {
        if c.Request != nil {
            c.queryCache = c.Request.URL.Query()
        } else {
            c.queryCache = url.Values{}
        }
    }
}
func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
    c.initQueryCache()
    values, ok = c.queryCache[key]
    return
}

通过
c.JSON
返回
Content-Type

application/json
的响应体,

这也是
Gin
对原生net/http编程的一个优化,对常用的响应类型进行封装,方便使用者使用。

当然,Gin对请求/响应参数的处理还有其它很多细微的优化,这里就不详细说明了。

5. 总结

Gin使用Map来实现路由匹配,而Gin使用路由树来实现路由匹配,支持动态路由,内存占用小且路由匹配快。

同时Gin使用缓存来优化请求参数的处理过程,提供了通用的响应参数处理等,方便用户使用。

1. 什么是Openssl?

在计算机网络上,OpenSSL是一个开放源代码的软件库包,应用程序可以使用这个包来进行安全通信,避免窃听,同时确认另一端连线者的身份。这个包广泛被应用在互联网的网页服务器上。

其主要库是以C语言所写成,实现了基本的加密功能,实现了SSL与TLS协议。OpenSSL可以运行在OpenVMS、 Microsoft Windows以及绝大多数类Unix操作系统上(包括Solaris,Linux,Mac OS X与各种版本的开放源代码BSD操作系统)。

虽然此软件是开放源代码的,但其许可书条款与GPL有冲突之处,故GPL软件使用OpenSSL时(如Wget)必须对OpenSSL给予例外。
https://www.openssl.org/

2. 什么是心脏滴血?

心脏出血漏洞(英语:Heartbleed bug),简称为心血漏洞,是一个出现在加密程序库OpenSSL的安全漏洞,该程序库广泛用于实现互联网的传输层安全(TLS)协议。它于2012年被引入了OpenSSL中,2014年4月首次向公众披露。只要使用的是存在缺陷的OpenSSL实例,无论是服务器还是客户端,都可能因此而受到攻击。此问题的原因是在实现TLS的心跳扩展时没有对输入进行适当验证(缺少边界检查,因此漏洞的名称来源于“心跳”(heartbeat。该程序错误属于缓冲区过读,即可以读取的数据比应该允许读取的还多。

心脏出血在通用漏洞披露(CVE)系统中的编号为CVE-2014-0160。加拿大网络事故响应中心发布安全公告,提醒系统管理员注意漏洞。2014年4月7日,即漏洞公开披露的同一天,OpenSSL发布了修复后的版本。
截至2014年5月20日,在80万最热门的启用TLS的网站中,仍有1.5%易受心脏出血漏洞的攻击。
因为缺陷在于OpenSSL的实现,而不是SSL/TLS协议本身,所以除了OpenSSL之外的其他TLS实现方式,如GnuTLS、Mozilla的网络安全服务(NSS)和Windows平台的TLS实现都不受影响。

3. Nginx升级openssl

3.1 查看现openssl版本

# nginx -V
nginx version: nginx/1.22.1
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) 
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled

3.2 官方下载新的openssl安装包并解压

也可在GitHub上下载:
https://github.com/openssl/openssl/releases

# wget https://www.openssl.org/source/openssl-3.0.8.tar.gz -P /opt/ --no-check-certificate
]# tar -xf openssl-3.0.8.tar.gz 

3.3 重新编译Nginx

# ./configure  --prefix=/apps/nginx --user=nginx --group=nginx --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_stub_status_module --with-http_gzip_static_module --with-pcre --with-stream --with-stream_ssl_module --with-stream_realip_module --with-openssl=/opt/openssl-3.0.8

3.4 编译时报错

# make 
make -f objs/Makefile
make[1]: 进入目录“/usr/local/src/nginx-1.22.1”
cd /opt/openssl-3.0.8 \
&& if [ -f Makefile ]; then make clean; fi \
&& ./config --prefix=/opt/openssl-3.0.8/.openssl no-shared no-threads  \
&& make \
&& make install_sw LIBDIR=lib
Can't locate IPC/Cmd.pm in @INC (@INC contains: /opt/openssl-3.0.8/util/perl /usr/local/lib64/perl5 /usr/local/share/perl5 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5 . /opt/openssl-3.0.8/external/perl/Text-Template-1.56/lib) at /opt/openssl-3.0.8/util/perl/OpenSSL/config.pm line 19.
BEGIN failed--compilation aborted at /opt/openssl-3.0.8/util/perl/OpenSSL/config.pm line 19.
Compilation failed in require at /opt/openssl-3.0.8/Configure line 23.
BEGIN failed--compilation aborted at /opt/openssl-3.0.8/Configure line 23.
make[1]: *** [/opt/openssl-3.0.8/.openssl/include/openssl/ssl.h] 错误 2

3.5 解决方法

yum install -y perl-CPAN
[root@haitang-nginx-test openssl-3.0.8]# perl -MCPAN -e shell

CPAN.pm requires configuration, but most of it can be done automatically.
If you answer 'no' below, you will enter an interactive dialog for each
configuration option instead.

Would you like to configure as much as possible automatically? [yes] yes

 <install_help>

Warning: You do not have write permission for Perl library directories.

To install modules, you need to configure a local Perl library directory or
escalate your privileges.  CPAN can help you by bootstrapping the local::lib
module or by configuring itself to use 'sudo' (if available).  You may also
resolve this problem manually if you need to customize your setup.

What approach do you want?  (Choose 'local::lib', 'sudo' or 'manual')

3.6 安装缺省的包

cpan[1]> install IPC/Cmd.pm 
..............................................................DONE
Fetching with HTTP::Tiny:
http://www.cpan.org/modules/03modlist.data.gz
Reading '/root/.cpan/sources/modules/03modlist.data.gz'
DONE
Writing /root/.cpan/Metadata
Running install for module 'IPC::Cmd'
Running make for B/BI/BINGOS/IPC-Cmd-1.04.tar.gz
Fetching with HTTP::Tiny:
http://www.cpan.org/authors/id/B/BI/BINGOS/IPC-Cmd-1.04.tar.gz
Fetching with HTTP::Tiny:
http://www.cpan.org/authors/id/B/BI/BINGOS/CHECKSUMS
Checksum for /root/.cpan/sources/authors/id/B/BI/BINGOS/IPC-Cmd-1.04.tar.gz ok
Scanning cache /root/.cpan/build for sizes
DONE

  CPAN.pm: Building B/BI/BINGOS/IPC-Cmd-1.04.tar.gz

Checking if your kit is complete...
Looks good
Warning: prerequisite Locale::Maketext::Simple 0 not found.
Warning: prerequisite Module::Load::Conditional 0.66 not found.
Warning: prerequisite Params::Check 0.20 not found.
Warning: prerequisite Test::More 0 not found.
Writing Makefile for IPC::Cmd
Could not read metadata file. Falling back to other methods to determine prerequisites
---- Unsatisfied dependencies detected during ----
----        BINGOS/IPC-Cmd-1.04.tar.gz        ----
    Test::More [requires]
    Locale::Maketext::Simple [requires]
    Module::Load::Conditional [requires]
    Params::Check [requires]

3.7 安装完成继续执行编译操作。

# ./configure  --prefix=/apps/nginx --user=nginx --group=nginx --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_stub_status_module --with-http_gzip_static_module --with-pcre --with-stream --with-stream_ssl_module --with-stream_realip_module --with-openssl=/opt/openssl-3.0.8
checking for OS
 + Linux 3.10.0-1062.el7.x86_64 x86_64
checking for C compiler ... found
 + using GNU C compiler
 + gcc version: 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) 
checking for gcc -pipe switch ... found
checking for -Wl,-E switch ... found
checking for gcc builtin atomic operations ... found
checking for C99 variadic macros ... found
checking for gcc variadic macros ... found
checking for gcc builtin 64 bit byteswap ... found

3.8 执行make && make install

# make && make install
cp objs/nginx '/apps/nginx/sbin/nginx'
test -d '/apps/nginx/conf' \
	|| mkdir -p '/apps/nginx/conf'
cp conf/koi-win '/apps/nginx/conf'
cp conf/koi-utf '/apps/nginx/conf'
cp conf/win-utf '/apps/nginx/conf'
test -f '/apps/nginx/conf/mime.types' \
	|| cp conf/mime.types '/apps/nginx/conf'
cp conf/mime.types '/apps/nginx/conf/mime.types.default'
test -f '/apps/nginx/conf/fastcgi_params' \
	|| cp conf/fastcgi_params '/apps/nginx/conf'
cp conf/fastcgi_params \
	'/apps/nginx/conf/fastcgi_params.default'
test -f '/apps/nginx/conf/fastcgi.conf' \
	|| cp conf/fastcgi.conf '/apps/nginx/conf'
cp conf/fastcgi.conf '/apps/nginx/conf/fastcgi.conf.default'
test -f '/apps/nginx/conf/uwsgi_params' \
	|| cp conf/uwsgi_params '/apps/nginx/conf'
cp conf/uwsgi_params \
	'/apps/nginx/conf/uwsgi_params.default'
test -f '/apps/nginx/conf/scgi_params' \
	|| cp conf/scgi_params '/apps/nginx/conf'
cp conf/scgi_params \
	'/apps/nginx/conf/scgi_params.default'
test -f '/apps/nginx/conf/nginx.conf' \
	|| cp conf/nginx.conf '/apps/nginx/conf/nginx.conf'
cp conf/nginx.conf '/apps/nginx/conf/nginx.conf.default'
test -d '/apps/nginx/logs' \
	|| mkdir -p '/apps/nginx/logs'
test -d '/apps/nginx/logs' \
	|| mkdir -p '/apps/nginx/logs'
test -d '/apps/nginx/html' \
	|| cp -R html '/apps/nginx'
test -d '/apps/nginx/logs' \
	|| mkdir -p '/apps/nginx/logs'
make[1]: 离开目录“/usr/local/src/nginx-1.22.1”

3.9 查看是否升级成功

# nginx -V
nginx version: nginx/1.22.1
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) 
built with OpenSSL 3.0.8 7 Feb 2023
TLS SNI support enabled

特权升级是一段旅程。没有灵丹妙药,很大程度上取决于目标系统的具体配置。内核版本、已安装的应用程序、支持的编程语言、其他用户的密码是影响您通往 root shell 之路的几个关键因素

什么是特权升级?

其核心是,特权升级通常涉及从权限较低的帐户到权限较高的帐户。从技术上讲,它是利用操作系统或应用程序中的漏洞、设计缺陷或配置疏忽来获得对通常限制用户访问的资源的未授权访问。

它为什么如此重要?

在执行真实世界的渗透测试时,很少能够获得立足点(初始访问权限),从而为您提供直接的管理访问权限。特权升级至关重要,因为它可以让您获得系统管理员级别的访问权限,从而允许您执行以下操作:

  • 重置密码
  • 绕过访问控制以破坏受保护的数据
  • 编辑软件配置
  • 启用持久性
  • 更改现有(或新)用户的权限
  • 执行任何管理命令

枚举

一旦您获得对任何系统的访问权限,枚举是您必须采取的第一步。您可能已经通过利用导致根级别访问的严重漏洞访问了系统,或者只是找到了一种使用低特权帐户发送命令的方法。与 CTF 机器不同,渗透测试活动不会在您获得对特定系统或用户权限级别的访问权限后结束。正如您将看到的,枚举在妥协后阶段和之前一样重要

hostname

hostname
命令将返回目标机器的主机名。尽管此值可以很容易地更改或具有相对无意义的字符串(例如 Ubuntu-3487340239),但在某些情况下,它可以提供有关目标系统在公司网络中的角色的信息(例如用于生产 SQL 服务器的 SQL-PROD-01)

uname -a
将打印系统信息,为我们提供有关系统使用的内核的更多详细信息。这在搜索任何可能导致特权升级的潜在内核漏洞时非常有用

/proc/version
proc 文件系统
(procfs)
提供有关目标系统进程的信息。您会在许多不同的 Linux 风格上找到
proc
,这使它成为您武器库中必不可少的工具。

查看
/proc/version
可能会为您提供有关内核版本和其他数据的信息,例如是否安装了编译器(例如 GCC)

/etc/issue
也可以通过查看/etc/issue文件来识别系统。该文件通常包含有关操作系统的一些信息,但可以很容易地进行自定义或更改。在主题上,可以自定义或更改任何包含系统信息的文件。为了更清楚地了解系统,查看所有这些总是好的

ps
ps
命令是查看 Linux 系统上正在运行的进程的有效方法。在您的终端上键入ps 将显示当前 shell 的进程

(进程状态)的输出
ps
将显示以下内容:

  • PID:进程ID(进程唯一)
  • TTY:用户使用的终端类型
  • TIME:进程使用的 CPU 时间量(这不是该进程运行的时间)
  • CMD:正在运行的命令或可执行文件(不会显示任何命令行参数)

“ps”命令提供了一些有用的选项。

  • ps -A: 查看所有正在运行的进程
  • ps axjf:查看进程树(见下面的树形成直到ps axjf运行)
  • ps aux:该aux 选项将显示所有用户的进程 (a),显示启动进程的用户 (u),并显示未附加到终端的进程 (x)。查看 ps aux 命令输出,我们可以更好地了解系统和潜在漏洞

env 和 export

env
命令将显示环境变量

PATH
变量可能具有编译器或脚本语言(例如
Python
),可用于在目标系统上运行代码或用于特权升级

sudo -l
目标系统可以配置为允许用户以 root 权限运行一些(或所有)命令。该
sudo -l
命令可用于列出您的用户可以使用运行的所有命令sudo

ls
Linux 中使用的常用命令之一可能是
ls

在寻找潜在的特权升级向量时,请记住始终使用
ls
带有参数的命令
-la
。下面使用
ls -l
遗漏了隐藏文件“secret.txt”

id
该id 命令将提供用户权限级别和组成员身份的总体概览。

值得记住的是,该id 命令也可以获取其他用户的信息:

/etc/passwd
读取
/etc/passwd
文件是发现系统用户的一种简单方法

虽然输出可能很长而且有点吓人,但它可以很容易地被剪切并转换成一个有用的列表以用于暴力攻击

cat /etc/passwd | cut -d ":" -f 1

请记住,这将返回所有用户,其中一些是不是很有用的系统或服务用户。另一种方法可能是
grep
查找
“home”
,因为真实用户很可能将他们的文件夹放在
“home”
目录下

cat /etc/passwd | grep home

history
使用命令查看较早的命令 history 可以让我们对目标系统有一些了解,尽管很少见,但可能存在密码或用户名等信息

ifconfig

目标系统可能是另一个网络的枢轴点。该
ifconfig
命令将为我们提供有关系统网络接口的信息。下面的示例显示目标系统具有三个接口(eth0、tun0 和 tun1)。我们的攻击机器可以到达 eth0 接口,但不能直接访问其他两个网

可以使用ip route 命令查看存在哪些网络路由来确认这一点

netstat
在对现有接口和网络路由进行初步检查后,值得研究现有通信。该
netstat
命令可以与几个不同的选项一起使用,以收集有关现有连接的信息

  • netstat -a
    :显示所有侦听端口和已建立的连接。
  • netstat -at
    或者
    netstat -au
    也可用于分别列出 TCP 或 UDP 协议。
  • netstat -l
    :列出处于“侦听”模式的端口。这些端口已打开并准备好接受传入连接。这可以与“t”选项一起使用,以仅列出使用 TCP 协议侦听的端口(如下)
  • netstat -s -t
    : 按协议列出网络使用统计信息(如下) 这也可以与或选项一起使用,
    -u
    以将输出限制为特定协议
  • netstat -tp
    :列出带有服务名称和
    PID
    信息的连接

-l
这也可以与列出监听端口的选项 一起使用(如下)

我们可以看到
“PID/Program name”
列是空的,因为这个进程属于另一个用户。

下面是使用
root
权限运行的相同命令,并将此信息显示为
2641/nc (netcat)

  • netstat -i
    :显示接口统计信息。我们在下面看到“eth0”和“tun0”比“tun1”更活跃

netstat 您可能会在博客文章、文章和课程中最常看到的用法可以 细分
netstat -ano
如下:

  • -a: 显示所有
  • -n: 不解析名称
  • -o:显示定时器

建议使用:

netstat -anpt

netstat -anpte

netstat -tunple

find
在目标系统中搜索重要信息和潜在的特权升级向量可能会很有成效。内置的
“find”
命令很有用,值得保留在您的武器库中

以下是“查找”命令的一些有用示例

查找文件:

  • find . -name flag1.txt
    : 在当前目录下找到名为“flag1.txt”的文件
  • find /home -name flag1.txt
    : 在/home目录下找到文件名“flag1.txt”
  • find / -type d -name config
    : 在“/”下找到名为config的目录
  • find / -type f -perm 0777
    : 查找777权限的文件(所有用户可读、可写、可执行的文件)
  • find / -perm a=x
    : 查找可执行文件
  • find /home -user frank
    : 在“/home”下查找用户“frank”的所有文件
  • find / -mtime 10
    :查找最近 10 天内修改过的文件
  • find / -atime 10
    :查找最近 10 天内访问过的文件
  • find / -cmin -60
    :查找最近一小时(60 分钟)内更改的文件
  • find / -amin -60
    :查找最近一小时(60 分钟)内的文件访问
  • find / -size 50M
    :查找大小为 50 MB 的文件

此命令还可以与
(+)

(-)
符号一起使用,以指定大于或小于给定大小的文件

上面的示例返回大于 100 MB 的文件。重要的是要注意“find”命令往往会产生错误,有时会使输出难以阅读。这就是为什么明智的做法是使用带有
“-type f 2>/dev/null”
的“find”命令将错误重定向到
“/dev/null”
并获得更清晰的输出(如下)

可以写入或执行的文件夹和文件:

  • find / -writable -type d 2>/dev/null
    :查找可写文件夹
  • find / -perm -222 -type d 2>/dev/null
    :查找可写文件夹
  • find / -perm -o=w -type d 2>/dev/null
    :查找可写文件夹
  • find / -perm -o=x -type d 2>/dev/null
    :查找可执行文件夹

查找开发工具和支持的语言:

  • find / -name perl*
  • find / -name python*
  • find / -name gcc*

查找特定文件权限:

下面是一个简短的示例,用于查找设置了 SUID 位的文件。SUID 位允许文件以拥有它的帐户的特权级别运行,而不是运行它的帐户。这允许一个有趣的权限升级路径:

  • find / -perm -u=s -type f 2>/dev/null
    :查找带有SUID位的文件,这样我们就可以运行比当前用户更高权限级别的文件
  • find / -perm -04000 -print 2>/dev/null
    : 也是查找SUID文件
  • find / -perm -04000 -print 2>/dev/null
    :也是查找SUID文件

通用 Linux 命令
由于我们在 Linux 领域,熟悉 Linux 命令通常会非常有用。请花一些时间熟悉
find
,
locate
,
grep
,
cut
,
sort
等命令

自动枚举工具

有几种工具可以帮助您在枚举过程中节省时间。这些工具应该只用于节省时间,因为它们可能会错过一些特权升级向量。下面是一个流行的 Linux 枚举工具列表,带有指向它们各自 Github 存储库的链接

目标系统的环境将影响您将能够使用的工具。例如,如果目标系统上没有安装用 Python 编写的工具,您将无法运行它。这就是为什么最好熟悉一些工具而不是拥有一个单一的首选工具

内核漏洞利用

理想情况下,权限升级会导致 root 权限。这有时可以简单地通过利用现有漏洞来实现,或者在某些情况下通过访问另一个具有更多权限、信息或访问权限的用户帐户来实现。

除非单个漏洞导致
root shell
,否则权限升级过程将依赖于错误配置和松散的权限。

Linux 系统上的内核管理组件之间的通信,例如系统上的内存和应用程序。这个关键功能需要内核有特定的权限;因此,成功的利用可能会导致 root 特权。

内核利用方法很简单;

识别内核版本
搜索并找到目标系统内核版本的漏洞利用代码
运行漏洞
虽然看起来很简单,但请记住,内核漏洞利用失败可能会导致系统崩溃。在尝试内核利用之前,请确保这种潜在结果在您的渗透测试范围内是可以接受的。

研究来源:

  • 根据您的发现,您可以使用 Google 搜索现有的漏洞利用代码。
  • https://www.linuxkernelcves.com/cves等来源也很有用。
  • 另一种选择是使用像 LES (Linux Exploit Suggester) 这样的脚本,但请记住,这些工具可能会产生误报(报告不影响目标系统的内核漏洞)或漏报(不报告任何内核漏洞,尽管内核是易受伤害的)。

提示/注意事项:

  • 在 Google、Exploit-db 或 searchsploit 上搜索漏洞时,对内核版本过于具体
  • 在启动之前,请务必了解漏洞利用代码的工作原理。一些漏洞利用代码可以对操作系统进行更改,使它们在进一步使用时不安全,或者对系统进行不可逆的更改,从而在以后产生问题。当然,在实验室或 CTF 环境中,这些可能不是什么大问题,但在真正的渗透测试过程中,这些绝对不能。
  • 一些漏洞利用在运行后可能需要进一步的交互。阅读漏洞利用代码提供的所有注释和说明。
  • SimpleHTTPServer
    您可以分别使用
    Python
    模块和将漏洞利用代码从您的机器传输到目标系统
    wget

SUDO

默认情况下,sudo 命令允许您以 root 权限运行程序。在某些情况下,系统管理员可能需要为普通用户提供一些灵活的权限。例如,初级 SOC 分析师可能需要定期使用 Nmap,但不会获得完全根访问权限。在这种情况下,系统管理员可以允许该用户仅以 root 权限运行 Nmap,同时在系统的其余部分保持其常规权限级别

任何用户都可以使用该
sudo -l
命令查看其当前与root权限相关的情况

https://gtfobins.github.io/
是一个有价值的资源,它提供了有关如何使用您可能拥有 sudo 权限的任何程序的信息

利用应用程序功能
一些应用程序在此上下文中不会有已知的漏洞利用。您可能会看到这样的应用程序是 Apache2 服务器

在这种情况下,我们可以使用“hack”来利用应用程序的功能来泄露信息。如下所示,Apache2 有一个选项支持加载备用配置文件(-f:指定备用 ServerConfigFile)

使用此选项加载/etc/shadow文件将导致包含文件第一行的错误消息
/etc/shadow

SUID

许多 Linux 权限控制依赖于控制用户和文件交互。这是通过权限完成的。到目前为止,您知道文件可以具有读取、写入和执行权限。这些是在其权限级别内提供给用户的。这随着 SUID(设置用户标识)和 SGID(设置组标识)而改变。这些允许文件分别以文件所有者或组所有者的权限级别执行

您会注意到这些文件设置了一个
“s”
位来显示它们的特殊权限级别

find / -type f -perm -04000 -ls 2>/dev/null
将列出设置了 SUID 或 SGID 位的文件。

一个好的做法是将此列表中的可执行文件与 GTFOBins (
https://gtfobins.github.io
) 进行比较。单击 SUID 按钮将在设置 SUID 位时过滤已知可利用的二进制文件(您也可以使用此链接获取预过滤列表
https://gtfobins.github.io/#+suid)

上面的列表显示
nano
设置了SUID位。不幸的是,GTFObins并没有为我们带来轻松的胜利。对于现实生活中的特权升级场景,我们需要找到中间步骤,以帮助我们利用我们所获得的任何微小发现

为 nano 文本编辑器设置的 SUID 位允许我们使用文件所有者的权限创建、编辑和读取文件。Nano 由
root
拥有,这可能意味着我们可以以比当前用户更高的权限级别读取和编辑文件。在这个阶段,我们有两个基本的权限提升选项:读取文件
/etc/shadow
或将我们的用户添加到
/etc/passwd

以下是使用这两种载体的简单步骤。

读取
/etc/shadow
文件

我们通过运行
find / -type f -perm -04000 -ls 2>/dev/null
命令看到 nano 文本编辑器设置了
SUID

nano /etc/shadow
将打印文件的内容
/etc/shadow
。我们现在可以使用
unshadow
工具创建一个可被
John the Ripper
破解的文件。为此,
unshadow
需要
/etc/shadow

/etc/passwd
文件

unshadow 工具的用法如下所示;
unshadow passwd.txt shadow.txt > passwords.txt

有了正确的单词表和一点运气,开膛手约翰(john)可以爆破并返回明文形式一个或多个密码

另一种选择是添加一个具有 root 权限的新用户。这将帮助我们规避繁琐的密码破解过程。下面是一个简单的方法来做到这一点:

我们将需要我们希望新用户拥有的密码的哈希值。这可以使用 Kali Linux 上的 openssl 工具快速完成。

openssl passwd -1 -salt 用户名 密码

然后,我们会将此密码和用户名添加到
/etc/passwd
文件中

一旦我们的用户被添加(请注意我们
root:/bin/bash
是如何提供一个
root shell
的)我们将需要切换到这个用户并且希望应该有
root 权限

然后切换到该用户,我们就拥有了
root
权限

Capabilities(Capabilities)

系统管理员可以用来提高进程或二进制文件特权级别的另一种方法是“Capabilities”。功能有助于在更精细的级别管理权限。例如,如果 SOC 分析师需要使用需要发起套接字连接的工具,普通用户将无法做到这一点。如果系统管理员不想给这个用户更高的权限,他们可以更改二进制文件的功能。因此,二进制文件无需更高权限的用户即可完成任务。
功能手册页提供了有关其用法和选项的详细信息

我们可以使用该
getcap
工具列出启用的功能

当以非特权用户身份运行时,
getcap -r /
会产生大量错误,因此最好将错误消息重定向到
/dev/null

请注意,
vim
及其副本都没有设置
SUID
位。因此,在枚举查找
SUID
的文件时,无法发现此特权升级向量

GTFObins 有一个很好的二进制文件列表,如果我们发现任何设置的功能,可以利用这些二进制文件进行特权升级

我们注意到 vim 可以与以下命令和有效负载一起使用:

这将启动一个 root shell,如下所示:

Cron 作业

Cron 作业用于在特定时间运行脚本或二进制文件。默认情况下,它们以其所有者而非当前用户的权限运行。虽然正确配置的 cron 作业本身并不容易受到攻击,但它们可以在某些情况下提供特权升级向量。
这个想法很简单;如果有一个以 root 权限运行的计划任务,并且我们可以更改将要运行的脚本,那么我们的脚本将以 root 权限运行

Cron 作业配置存储为 crontab(cron 表)以查看任务将运行的下一个时间和日期。

系统上的每个用户都有他们的 crontab 文件,并且无论他们是否登录都可以运行特定的任务。如您所料,我们的目标是找到一个由 root 设置的 cron 作业并让它运行我们的脚本,最好是一个 shell

任何用户都可以阅读保存系统范围 cron 作业的文件
/etc/crontab

虽然 CTF 机器可以让 cron 作业每分钟或每 5 分钟运行一次,但在渗透测试活动中,您会更经常看到每天、每周或每月运行的任务

您可以看到
backup.sh
脚本被配置为每分钟运行一次。该文件的内容显示了一个创建
prices.xls
文件备份的简单脚本

由于我们当前的用户可以访问这个脚本,我们可以很容易地修改它来创建一个反向 shell,希望具有 root 权限

该脚本将使用目标系统上可用的工具来启动反向 shell。
需要注意两点:

  • 命令语法会因可用工具而异。(例如
    nc
    ,可能不支持
    -e
    您在其他情况下使用过的选项)
  • 我们应该总是更喜欢启动反向 shell,因为我们不想在真正的渗透测试过程中损害系统的完整性。

该文件应如下所示;

我们现在将在我们的攻击机器上运行一个监听器来接收传入的连接。

Crontab 始终值得检查,因为它有时会导致简单的特权升级向量。以下场景在不具备一定网络安全成熟度级别的公司中并不少见:

  1. 系统管理员需要定期运行脚本。
  2. 他们创建了一个 cron 作业来执行此操作
  3. 一段时间后,脚本变得无用,他们将其删除
  4. 他们不清理相关的 cron 作业

此变更管理问题导致利用 cron 作业的潜在漏洞

上面的示例显示了类似的情况,其中删除了
antivirus.sh
脚本,但
cron
作业仍然存在。
如果未定义脚本的完整路径(如对
backup.sh
脚本所做的那样),
cron
将引用
/etc/crontab
文件中
PATH
变量下列出的路径。在这种情况下,我们应该能够在用户的主文件夹下创建一个名为
“antivirus.sh”
的脚本,它应该由 cron 作业运行

目标系统上的文件应该看起来很熟悉:

传入的反向 shell 连接具有 root 权限:

在奇怪的情况下,您会发现一个现有的脚本或任务附加到 cron 作业,花时间了解脚本的功能以及如何在上下文中使用任何工具总是值得的。例如,
tar、7z、rsync
等,可以使用它们的通配符功能进行利用

PATH

如果您的用户具有写入权限的文件夹位于路径中,您可能会劫持应用程序来运行脚本。Linux 中的 PATH 是一个环境变量,它告诉操作系统在哪里搜索可执行文件。对于任何未内置于 shell 中或未使用绝对路径定义的命令,Linux 将开始在 PATH 下定义的文件夹中搜索。(这里说的PATH是环境变量,path是文件所在的位置)

通常 PATH 看起来像这样:

如果我们在命令行中键入“thm”,Linux 将在这些位置查找名为 thm 的可执行文件。下面的场景将使您更好地了解如何利用它来提高我们的特权级别。正如您将看到的,这完全取决于目标系统的现有配置,因此请确保您在尝试之前能够回答以下问题:

  1. $PATH
    下有哪些文件夹
  2. 您当前的用户是否对这些文件夹中的任何一个具有写入权限?
  3. 你能修改$PATH吗?
  4. 是否有您可以启动的脚本/应用程序会受此漏洞影响?

出于演示目的,我们将使用以下脚本:

该脚本尝试启动一个名为“thm”的系统二进制文件,但该示例可以很容易地用任何二进制文件复制

我们将其编译成可执行文件并设置 SUID 位

我们的用户现在可以访问设置了 SUID 位的
“路径”
脚本。

如果 PATH 下列出了任何可写文件夹,我们可以在该目录下创建一个名为 thm 的二进制文件,并让我们的“路径”脚本运行它。由于设置了 SUID 位,此二进制文件将以 root 权限运行

使用
“find”
命令可以完成对可写文件夹的简单搜索
find / -writable 2>/dev/null
。可以使用简单的剪切和排序序列清理此命令的输出

一些 CTF 场景可以呈现不同的文件夹,但常规系统会输出如上所示的内容。

将其与 PATH 进行比较将帮助我们找到可以使用的文件夹

我们在
/usr
下看到许多文件夹,因此再次运行我们的可写文件夹搜索以覆盖子文件夹可能更容易

另一种方法是下面的命令。

find / -writable 2>/dev/null | cut -d "/" -f 2,3 | grep -v proc | sort -u

我们添加了“grep -v proc”以消除与运行进程相关的许多结果

不幸的是,
/usr
下的子文件夹不可写

更容易写入的文件夹可能是
/tmp
。此时因为
/tmp
不存在于
PATH
中,所以我们需要添加它。正如我们在下面看到的,
“ export PATH=/tmp:$PATH”
命令完成了这一点

此时,路径脚本还将在
/tmp
文件夹下查找名为“thm”的可执行文件。

通过将
/bin/bash
复制为
/tmp
文件夹下的
“thm”
,创建此命令相当容易。

我们已经为我们的
/bin/bash
副本授予了可执行权限,请注意,此时它将以我们用户的权限运行。在此上下文中使权限升级成为可能的原因是路径脚本以 root 权限运行

NFS

特权升级向量并不局限于内部访问。共享文件夹和远程管理界面(例如 SSH 和 Telnet)也可以帮助您获得目标系统的根访问权限。某些情况下还需要同时使用这两种方式,例如,在目标系统上找到根 SSH 私钥并使用根权限通过 SSH 连接,而不是尝试提高当前用户的权限级别

另一个与 CTF 和考试更相关的向量是错误配置的网络外壳。当存在网络备份系统时,有时可以在渗透测试过程中看到此向量

NFS(网络文件共享)配置保存在
/etc/exports
文件中。该文件是在 NFS 服务器安装期间创建的,通常可供用户读取

此权限升级向量的关键元素是您在上面看到的
“no_root_squash”
选项。默认情况下,NFS 会将 root 用户更改为
nfsnobody
并剥夺任何文件以
root
权限操作。如果可写共享上存在
“no_root_squash”
选项,我们可以创建一个设置了
SUID
位的可执行文件并在目标系统上运行它

我们将从枚举攻击机器的可挂载共享开始。

我们将把其中一个
“no_root_squash”
共享挂载到我们的攻击机器上并开始构建我们的可执行文件

由于我们可以设置 SUID 位,因此将在目标系统上运行
/bin/bash
的简单可执行文件将完成这项工作

编译代码后,我们将设置 SUID 位

您将在下面看到这两个文件(
nfs.c

nfs
存在于目标系统上。我们已经处理了挂载的共享,因此无需传输它们)